Pythonを使うようになって、少しずつ「デコレータ」も使う場面が出てきました。これまではフレームワーク等で用意されているものを定型句として使うだけでしたが、デコレータの正体が何なのか、そして自分でも簡単に作れるのかが気になってきました。
Pythonの公式ドキュメントを読んだところ、デコレータについての記述は少ないものの、意外と単純な仕組みのようでした。(高階関数の知識は必要としますが)
そこで、実際に以下の記述例が動くよう f1
と f2
を作ってみることにしました。本記事ではその作成の思考過程を纏めています。
@f1(arg)
@f2
def func(): pass
公式ドキュメントの内容
参考にしたドキュメントを抜粋します。
8. 複合文 (compound statement) / 8.7. 関数定義
…
関数定義は一つ以上の デコレータ 式でラップできます。デコレータ式は関数を定義するとき、関数定義の入っているスコープで評価されます。その結果は、関数オブジェクトを唯一の引数にとる呼び出し可能オブジェクトでなければなりません。関数オブジェクトの代わりに、返された値が関数名に束縛されます。複数のデコレータはネストして適用されます。例えば、以下のようなコード:
@f1(arg)
@f2
def func(): pass
は、だいたい次と等価です
def func(): pass
func = f1(arg)(f2(func))
ただし、前者のコードでは元々の関数を func
という名前へ一時的に束縛することはない、というところを除きます。
…
用語集 / decorator
(デコレータ) 別の関数を返す関数で、通常、 @wrapper
構文で関数変換として適用されます。デコレータの一般的な利用例は、 classmethod()
と staticmethod()
です。
デコレータの文法はシンタックスシュガーです。次の2つの関数定義は意味的に同じものです:
def f(arg):
...
f = staticmethod(f)
@staticmethod
def f(arg):
...
同じ概念がクラスにも存在しますが、あまり使われません。デコレータについて詳しくは、 関数定義 および クラス定義 のドキュメントを参照してください。
作成してみる
ドキュメントに「デコレータの文法はシンタックスシュガー」や「func = f1(arg)(f2(func))
」と書いてあるので、デコレータには専用の定義方法があるわけでなく、ただ高階関数を作ればいいだけのようです。
引数をとらない @f2
と、引数をとる @f1(arg)
を作ってみます。
引数なし
まずは簡単なほう。 f = f2(f)
というように、「関数を引数にとり同様の関数を返す」関数 f2
を作ってみます。
一番単純なのは、引数をそのまま返す関数です。
def f2(f):
return f
とはいえこれだとデコレータの意味が無いので、 f
をラップした別の関数を返すように改良します。(引数はとりあえず何にでも対応できるようにしています)
def f2(f):
def wrapper(*args, **kwargs):
print("decorated by @f2")
return f(*args, **kwargs)
return wrapper
これで完成です。
引数あり
引数ありの場合は、関数を実行した f1(arg)
が f2
と同様のオブジェクトになる必要があります。デコレータの適用を以下のように分解していいということです。
f1_arg = f1(arg)
@f1_arg
@f2
def func(): pass
一番単純なのは、先ほど作成した引数なし版 f2
を返すことでしょう。
def f1(*d_args, **d_kwargs):
return f2
f2
に代わる関数をその場で作れば以下のようになります。 f2
のときより関数が1段多くなっています。
def f1(*d_args, **d_kwargs):
def decorator(f):
def wrapper(*args, **kwargs):
print("decorated by @f1")
return f(*args, **kwargs)
return wrapper
return decorator
せっかく f1
は引数をとっているため、それを使うよう改良します。( f1
のスコープであれば wrapper
の中でも外でも使えます)
def f1(*d_args, **d_kwargs):
def decorator(f):
def wrapper(*args, **kwargs):
d_args_repr = list(map(repr, d_args))
d_args_repr.extend(f"{k}={repr(v)}" for k, v in d_kwargs.items())
print(f"decorated by @f1({', '.join(d_args_repr)})")
return f(*args, **kwargs)
return wrapper
return decorator
実行
全て繋げて実行してみます。
def f1(*d_args, **d_kwargs):
def decorator(f):
def wrapper(*args, **kwargs):
d_args_repr = list(map(repr, d_args))
d_args_repr.extend(f"{k}={repr(v)}" for k, v in d_kwargs.items())
print(f"decorated by @f1({', '.join(d_args_repr)})")
return f(*args, **kwargs)
return wrapper
return decorator
def f2(f):
def wrapper(*args, **kwargs):
print("decorated by @f2")
return f(*args, **kwargs)
return wrapper
# デコレータを使う
arg = "foobar"
@f1(arg)
@f2
def func(): pass
# 関数を実行する
func()
decorated by @f1('foobar')
decorated by @f2
きちんと自作のデコレータ @f1
, @f2
が func
に適用されました。
今回は関数実行前にprintするだけでしたが、 wrapper
の中を変えれば元の関数をリトライさせたりなど様々な装飾ができそうです。どのような使い道があるのかは、ライブラリなどの実装を見て勉強していこうと思います。
(おまけ)ChatGPTに作ってもらった
自分で何となく作った上記コードが妥当か答え合わせしたくて、ChatGPTにもほぼ同じ条件でリクエストしてみました。だいたい同じコードが作られました。
問い合わせ
以下のpythonコードが動くように f1 と f2 のサンプルを作ってください。
@f1(arg)
@f2
def func(): pass
返答
まず、 @f1(arg)
と @f2
が何かしらの処理を行うデコレータであると考えられます。例えば、 f1
が引数を受け取り、それに応じた処理を行うデコレータであり、 f2
が引数を受け取らず固定の処理を行うデコレータということになります。
それぞれのデコレータのサンプルを記述してみましょう。
def f1(arg):
def decorator_func(func):
def wrapper():
print("f1 with arg:", arg)
func()
return wrapper
return decorator_func
def f2(func):
def wrapper():
print("f2")
func()
return wrapper
@f1("example_arg")
@f2
def my_func():
print("Executing my_func")
my_func()
このコードでは、 f1
は引数を受け取り、それを出力する処理を行うデコレータとして定義されています。 f2
は引数を受け取らず、固定の出力を行う処理を行うデコレータとして定義されています。 my_func
関数にはこれらのデコレータが適用されており、実行時にそれぞれの処理が実行されることが確認できます。
上記のコードを実行することで、 f1
と f2
のサンプルが作成された状態となります。