wraptのドキュメントを読んでいて、
- オプション引数でデコレータをパラメータ化できる
- オプション引数を指定しないときは括弧を省略できる
- ただしオプション引数はキーワード引数として指定する必要あり
という興味深い性質を持つデコレータの書き方を知ったので紹介します。
def with_optional_arguments(wrapped=None, myarg1=1, myarg2=2):
if wrapped is None:
return functools.partial(with_optional_arguments,
myarg1=myarg1, myarg2=myarg2)
@wrapt.decorator
def wrapper(wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)
return wrapper(wrapped)
ですが、functools.wraps でも以下のように書けば同じことが実現できます。
def with_optional_arguments(wrapped=None, myarg1=1, myarg2=2):
if wrapped is None:
return functools.partial(with_optional_arguments,
myarg1=myarg1, myarg2=myarg2)
@functools.wraps(wrapped)
def wrapper(*args, **kwargs):
return wrapped(*args, **kwargs)
return wrapper
オプション引数を指定する場合
@with_optional_arguments(myarg1=3, myarg2=4)
def function():
pass
のように書けて
- まず、
with_optional_arguments(myarg1=3, myarg2=4)
が評価される- 引数 wrapped が指定されていないので partial で myarg1, myarg2 を束縛
- 続けて、その結果で function をデコレートする
function = with_optional_arguments(myarg1=1, myarg2=2)(function)
という流れでデコレータが適用されます。
一方オプション引数を指定しない場合は
@with_optional_arguments()
def function():
pass
と書くと
- まず、
with_optional_arguments()
が評価される- 引数 wrapped もmyarg1, myarg2 も指定されていないので partial で myarg1=1, myarg2=2 を束縛
- 続けて、その結果で function をデコレートする
function = with_optional_arguments()(function)
という処理になり、括弧を書かなかった場合
@with_optional_arguments
def function():
pass
は単に
- function をデコレート
function = with_optional_argument(function)
- デフォルト値の myarg1=1, myarg2=2 が使われる
という処理になり、結果は空の括弧をつけた場合と括弧をつけない場合で同等になります。
このデコレータの定義方法を利用すると例えば
- 最初はデコレータのパラメータ要らないだろうと考えて、使う側では括弧なしで書いていた。
- 後から拡張によりパラメータを足したくなったが、既存のオプション引数指定しない箇所に空括弧つけて回るのは面倒だし無駄な感じがする
という状況をいい感じに乗り越えることができます。
なお、利用時の注意点として、キーワード引数とするところを誤って位置引数で指定してしまうと、本来関数が渡るべき第一引数に位置引数が入るため、わかりにくいエラーになったり誤動作したりするので気をつけましょう。