Pythonにおけるデコレータにはメリットとデメリットがある。それらを解説しつつ、そのデメリットをうまいこと回避するようにしているライブラリVenusianの紹介につなげます。
デコレータについて
まずはおさらい
デコレータとは何か。一言で言えば関数をラップする関数を返す関数です。(以下、関数とメソッドを一括りに関数といいます。)
例えば、こんなメモ化デコレータ。
def memorize(func):
cache = {}
def _func(*args):
if args not in cache:
result = func(*args)
cache[args] = result
else:
print("hit cache!: %r" % (args,))
return cache[args]
return _func
@memorize
def sum(*args):
j = 0
for i in args:
j += i
return j
sum(1, 2, 3, 4)
sum(1, 2, 3)
sum(1, 2, 3, 4)
実行するとこうなります。
hit cache!: (1, 2, 3, 4)
先ほど述べた「関数をラップする関数を返す関数」をマップしてみましょう。
def memorize(func): # 関数 (4)
cache = {}
def _func(*args): # ラップする関数 (2)
if args not in cache:
result = func(*args) # 関数を (1)
cache[args] = result
else:
print("hit cache!: %r" % (args,))
return cache[args]
return _func # を返す (3)
(1)はラップされた、この場合だとsum(*args)関数ですね。 なお、デコレータ構文が存在しなかった以前のバージョンではこうやってました。
sum = memorize(sum)
デコレータの性質について
デコレータはJavaのアノテーションとシンタックスは似ていますが、その関数の読み込み時に評価されます。
def print1(func):
def _func():
func()
print(1)
return _func
@print1
def foo():
pass
このコードの場合、読み込み時に1が出力されます。foo()関数は定義されただけで、呼び出されてないことに気をつけてください。
複雑なデコレータ
こんなデコレータ付きの関数を見たこともあるでしょう。
@printn(10)
def foo():
pass
デコレータに括弧がついてますね。これは括弧が付いてないデコレータと何が違うのでしょうか。
種も仕掛けもないのでコードをのせます。
def printn(n):
def _print(func):
def _func():
func()
print(n)
return _func
return _print
一言で言うなら「関数をラップする関数を返す関数「を返す関数」」です。これでデコレータに渡すパラメータを、関数の定義時に切り替えることができますね。
要件によって、このようにもデコレータをかけること、ありますよね。
@foo
@bar
def baz():
pass
や
@foo('bar')('baz')('qux')
def quux():
pass
後者はまだ見たことがありませんが、こんなことになる前に何とかしましょう。:-)
デコレータのメリットとデメリット
雰囲気はつかめたでしょうか。デコレータには関数の主たる機能以外の付随的な機能をもたせると、関心事の分離ができコードの見通しが良くなります。login_requiredなどはビューの主たる機能ではないですよね。これがメリット。
ただ残念なことに、デコレータはその定義の仕方から、オリジナルの関数の参照を隠してしまうという欠点があります。
sum = memorize(sum)
デコレータ構文を使っても実質的にはこれと変わりません。オリジナルのsum(*args)関数は、もはやどこからもアクセスすることができなくなります。(実際にはクロージャなので中を見てくとかありますが、今回は本質的ではないので省きます。)
参照できなくなることの何が問題なのでしょうか。あなたがテストコードを書かないのであれば、問題になることはまずないでしょう。ただそれはまた別の問題を近い将来に引き起こします。
テストコードを書く人は、こういう問題にぶち当たったことがあるはずです。
@christmas_only # クリスマスの日だけ有効な
@login_required # ログインしているユーザだけ有効な
@dict_to_json # 辞書をJSON形式に変換したレスポンスを返す
def merry_christmas(request):
data = get_tree_data() # クリスマスツリーデータを辞書で返す
return data
このビューは「本質的にはクリスマスツリーのデータを返すビュー」です。しかし、問題点がいくつかありますね。
- クリスマスの日だけ有効だとすると他の日ではどうやってテストするのするのでしょうか。
- ログインしているユーザのデータを毎回作る?
- 辞書データをJSON形式へ変換しているので、テストコードで内容のアサーションをしようとするとjson.loads()で先に復元しなければなりません。
これらの問題点はその関数の主たる機能ではないが、付随しているがためにテストコードの設計に影響を与え続けます。これがデメリット。
もちろんそれらを回避する方法はあります。
def _merry_christmas(request): # こっちの関数をテストする
data = get_tree_data() # クリスマスツリーデータを辞書で返す
return data
@christmas_only # クリスマスの日だけ有効な
@login_required # ログインしているユーザだけ有効な
@dict_to_json # 辞書をJSON形式に変換したレスポンスを返す
def merry_christmas(request):
return _merry_christmas(request)
DjangoのClassBasedViewもデコレータの観点からは同じ回避方法です。
自作デコレータであれば、デコレータのアトリビュートにオリジナルの関数を紐付けるという方法もあります。
def dict_to_json(func):
def _wrap(*args, **kwargs):
d = func(*args, **kwargs)
return json.dumps(s)
dict_to_json.original_func = func # これならテストできる
return _wrap
だけど、ちょっと待ってください。そもそもデコレータがオリジナルの関数を置き換えなければ、こんな面倒なことはしないでよかったのです。そうでしょう?
Venusianについて
Venusianは主にフレームワーク作成者のためのライブラリですが、デコレータの使い方に特長があります。
誤解を恐れずに言うならば「デコレータをアノテーションのように使います」。
# jsonized_view.py
import json
import venusian
def jsonify(wrapped):
def callback(scanner, name, ob):
def jsonified(request):
result = wrapped(request)
return json.dumps(result)
scanner.registry[name] = jsonified
venusian.attach(wrapped, callback)
return wrapped
@jsonify
def object_view(request):
return request.as_dict()
Venusianは2段階に分けデコレータを解釈します。アタッチとスキャン。
アタッチ
上のコードのうちアタッチのフェーズで何をやっているのかみてみましょう。
def jsonify(wrapped):
def callback(scanner, name, ob):
...
venusian.attach(wrapped, callback) # コールバックをアタッチして
return wrapped # オリジナルの関数を返す
奇妙なことにデコレータに渡ってきたオリジナルの関数をそのまま返してます。 作用だけみると、ラップされていない関数がモジュール内に戻されて、配置されるのがわかります。
でもこれでうまくいくのでしょうか。
スキャン
Venusianはスキャナによるスキャンをおこなうことで、アタッチされた関数を走査します。
import venusian
import jsonized_view
registry = {}
scanner = venusian.Scanner(registry=registry)
scanner.scan(jsonized_view)
アタッチされた関数には、Venusian用のコールバックcallback(scanner, name, ob)が設定されているので、それをスキャナが呼びます。 あのコードだとregistryにデコレータでラップした関数を登録していますね。 スキャン後のregistryはこのようになるでしょうか。
{'object_view': <function jsonified at 0x1004ef230>}
結局どうなったの
整理してみましょう。
- 関数の定義箇所では、オリジナルの関数がそのまま配置されている。
- スキャナでスキャンした結果、関数とそのデコレータしたあとの関数の対が得られる。
このような状況になりました。
テストのしやすさは改善したでしょうか。モジュール内にはオリジナルの関数がそのまま配置されているので、テストではデコレータの作用を気にせずにコードを書けそうです。
実はVenusianでおこなう事はこれが全てです。 あとはスキャンした結果を利用するフレームワークが、自身でURLマッピングにそのデコレートされた関数を使用したりします。有名なものではPyramidがVenusianを採用していますね。
上で述べたように、このフレームワークの仕組みは、デコレータの中身はデコレートされた関数を返すことを要求しないので、関数のカテゴライズや、アノテーション付与のみの目的での利用など、幅広い利用パターンが考えられます。