Python でデコレータを利用する
Python のデコレータ (decorator) は 公式ドキュメントの用語集 で以下のように定義されています。
Note
(デコレータ) 別の関数を返す関数で、通常、 @wrapper 構文で関数変換として適用されます。デコレータの一般的な利用例は、 classmethod() と staticmethod() です。
デコレータの文法はシンタックスシュガーです。次の2つの関数定義は意味的に同じものです:
def f(arg):
...
f = staticmethod(f)
@staticmethod
def f(arg):
...
同じ概念がクラスにも存在しますが、あまり使われません。デコレータについて詳しくは、 関数定義 および クラス定義 のドキュメントを参照してください。
つまり、デコレータの実態は「関数を受け取り、関数を返す関数」と言えます。 もう少し分かりやすく噛み砕くと「関数の処理内容を変更せず、関数に機能を追加出来る仕組み」とも言えます。
関数も「オブジェクト」¶
Python では関数も「オブジェクト」として扱うことが出来ます。 例えば以下のソースコードを考えます。
1 2 3 4 |
|
これは以下のように書き換えることが出来ます。
1 2 3 4 5 |
|
4 行目で v = greeting
と変数 v
に greeting
を代入しています。 代入時は「関数を呼び出しているわけでは無い」為、括弧は指定しません。 5 行目で関数を実行していますが、このタイミングで括弧を指定します。 上記、いずれのソースコードも実行結果は以下です。
Hello, World!
関数内関数¶
Python では関数の中に、更に関数を定義することが出来ます。 これは公式ドキュメントの 8.7. 関数定義 で言及されており、一般には Inner Function や Nested Function と呼ばれています。 下記のソースコードでは 2 つの関数を定義しています。
- timestamp 関数
-
関数を引数として受け取り、変数
f
に格納する。 関数内関数を用いて、関数f
の実行前後でdatetime.datetime.now()
を実行し、現在時刻を表示する。 - greeting 関数
-
Hello, World
という文字列を表示する
14 行目で timestamp(greeting)()
を実行していますが、これは「timestamp
関数に対して、引数として greeting
関数を指定する。 greeting
関数の引数は ()
(=無し)」という意味になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
2023-12-02 21:00:59.568289
Hello, World
2023-12-02 21:00:59.568317
デコレータを使った書き換え¶
前のサンプルのように「関数の引数に・関数を指定する」実行方法はやや分かりづらいかも知れません。 このような場合はデコレータを使うことでやや見通しが良くなります。 デコレータは関数名の前に @関数名
を指定することで定義出来ます。 下記では 14 行目の greeting
関数に対して、13 行目で @timestamp
デコレータを指定しています。 この指定により、「timestamp
関数の引数として greeting
関数を呼び出す」という振る舞いをします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
実行結果は以下です。 「デコレータを使わないケース」「デコレータを使ったケース」いずれも本質的には同じ意味になり、実行結果は同じです。
2023-12-02 21:01:00.770590
Hello, World
2023-12-02 21:01:00.770623
返り値を持つメソッドをデコレーションする¶
返り値を持つメソッドをデコレーションする場合、デコレータ関数側で値を return
するようにします。 下記のサンプルコードでは 3 行目で「f
関数 (実態は greeting()
で得られた値の最後に !
を追加」し、それを 4 行目で return
しています。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
実行結果は以下の通りです。 11 行目では文字列 Hello, World
を指定していますが、デコレータによって処理された結果、!
の文字列が追加され、結果が Hello, World!
になっていることが分かります。
Hello, World!
メタデータを保持する¶
下記のコードでは 11 行目で greeting
関数の __name__
を表示しています。
1 2 3 4 5 6 7 8 9 10 11 |
|
しかし、結果は (greeting
では無く) デコレーション関数内関数名である wrapper
が得られます。 これは本来得たかった値ではありません。
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)
でデコレートしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
実行結果は下記です。 これで greeting
が得られるようになりました。
greeting