Python でデコレータを利用する
Python のデコレータ (decorator) は 公式ドキュメントの用語集 で以下のように定義されています。
Note
(デコレータ) 別の関数を返す関数で、通常、 @wrapper 構文で関数変換として適用されます。デコレータの一般的な利用例は、 classmethod() と staticmethod() です。
デコレータの文法はシンタックスシュガーです。次の2つの関数定義は意味的に同じものです:
同じ概念がクラスにも存在しますが、あまり使われません。デコレータについて詳しくは、 関数定義 および クラス定義 のドキュメントを参照してください。
つまり、デコレータの実態は「関数を受け取り、関数を返す関数」と言えます。 もう少し分かりやすく噛み砕くと「関数の処理内容を変更せず、関数に機能を追加出来る仕組み」とも言えます。
関数も「オブジェクト」¶
Python では関数も「オブジェクト」として扱うことが出来ます。 例えば以下のソースコードを考えます。
これは以下のように書き換えることが出来ます。
4 行目で v = greeting と変数 v に greeting を代入しています。 代入時は「関数を呼び出しているわけでは無い」為、括弧は指定しません。 5 行目で関数を実行していますが、このタイミングで括弧を指定します。 上記、いずれのソースコードも実行結果は以下です。
関数内関数¶
Python では関数の中に、更に関数を定義することが出来ます。 これは公式ドキュメントの 8.7. 関数定義 で言及されており、一般には Inner Function や Nested Function と呼ばれています。 下記のソースコードでは 2 つの関数を定義しています。
- timestamp 関数
-
関数を引数として受け取り、変数
fに格納する。 関数内関数を用いて、関数fの実行前後でdatetime.datetime.now()を実行し、現在時刻を表示する。 - greeting 関数
-
Hello, Worldという文字列を表示する
14 行目で timestamp(greeting)() を実行していますが、これは「timestamp 関数に対して、引数として greeting 関数を指定する。 greeting 関数の引数は () (=無し)」という意味になります。
import datetime
def timestamp(f):
def wrapper(*args, **keywords):
print(datetime.datetime.now())
f(*args, **keywords)
print(datetime.datetime.now())
return wrapper
def greeting():
print("Hello, World")
timestamp(greeting)()
デコレータを使った書き換え¶
前のサンプルのように「関数の引数に・関数を指定する」実行方法はやや分かりづらいかも知れません。 このような場合はデコレータを使うことでやや見通しが良くなります。 デコレータは関数名の前に @関数名 を指定することで定義出来ます。 下記では 14 行目の greeting 関数に対して、13 行目で @timestamp デコレータを指定しています。 この指定により、「timestamp 関数の引数として greeting 関数を呼び出す」という振る舞いをします。
import datetime
def timestamp(f):
def wrapper(*args, **keywords):
print(datetime.datetime.now())
f(*args, **keywords)
print(datetime.datetime.now())
return wrapper
@timestamp
def greeting():
print("Hello, World")
greeting()
実行結果は以下です。 「デコレータを使わないケース」「デコレータを使ったケース」いずれも本質的には同じ意味になり、実行結果は同じです。
返り値を持つメソッドをデコレーションする¶
返り値を持つメソッドをデコレーションする場合、デコレータ関数側で値を return するようにします。 下記のサンプルコードでは 3 行目で「f 関数 (実態は greeting() で得られた値の最後に ! を追加」し、それを 4 行目で return しています。
def exclamate(f):
def wrapper(*args, **keywords):
msg = f(*args, **keywords) + "!"
return msg
return wrapper
@exclamate
def greeting(message: str):
return message
msg = greeting("Hello, World")
print(msg)
実行結果は以下の通りです。 11 行目では文字列 Hello, World を指定していますが、デコレータによって処理された結果、! の文字列が追加され、結果が Hello, World! になっていることが分かります。
メタデータを保持する¶
下記のコードでは 11 行目で greeting 関数の __name__ を表示しています。
def exclamate(f):
def wrapper(*args, **keywords):
msg = f(*args, **keywords) + "!"
return msg
return wrapper
@exclamate
def greeting(message: str):
return message
print(greeting.__name__)
しかし、結果は (greeting では無く) デコレーション関数内関数名である wrapper が得られます。 これは本来得たかった値ではありません。
これを解消するには @functools.wraps デコレータを使います。 公式ドキュメントでは以下のように記載されています。
Note
これはラッパー関数を定義するときに update_wrapper() を関数デコレータとして呼び出す便宜関数です。これは partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated) と等価です。例えば:
>>>
from functools import wraps
def my_decorator(f):
@wraps(f)
def wrapper(*args, **kwds):
print('Calling decorated function')
return f(*args, **kwds)
return wrapper
@my_decorator
def example():
"""Docstring"""
print('Called example function')
example()
Calling decorated function
Called example function
example.__name__
'example'
example.__doc__
'Docstring'
下記では 5 行目で関数内関数 wrapper に @functools.wraps(f) でデコレートしています。
import functools
def exclamate(f):
@functools.wraps(f)
def wrapper(*args, **keywords):
msg = f(*args, **keywords) + "!"
return msg
return wrapper
@exclamate
def greeting(message: str):
return message
print(greeting.__name__)
実行結果は下記です。 これで greeting が得られるようになりました。