Python の特徴的な構文の一つにデコレータがある。
便利な機能なんだけど、最初はとっつきにくいかもしれない。
そこで、今回はデコレータについて一通り色々と書いてみる。
先に断っておくと、とても長い。
これを読むと、以下が分かる。
- デコレータの本質
- デコレータはシンタックスシュガー (糖衣構文) に過ぎない
- デコレータの作り方
- デコレータの用途
- デコレータの種類
- デコレータの対象
- デコレートできるのは関数、メソッド以外にクラスもある
今回使った環境は次の通り。
尚、紹介するコードの中には、一部に Python 3 以降でないと動作しないものが含まれている。
$ python -V
Python 3.6.6
デコレータについて
まずはデコレータのおさらかいから。
デコレータは、その名の通りオブジェクトをデコレーション (装飾) するための機能。
構文としては、デコレートしたいオブジェクトの前で @
を先頭につけて使う。
デコレートできるオブジェクトの種類は、関数、メソッド、クラスがサポートされている。
標準モジュールにも、組み込みでいくつかのデコレータがある。
その中の一つを見てみよう。
以下のサンプルコードでは functools
モジュールの lru_cache
というデコレータを使っている。
このデコレータを使うと、デコレートした関数を簡単にメモ化できる。
メモ化というのは、ようするに関数の戻り値をキャッシュすること。
サンプルコードでは足し算をする add()
という関数をメモ化している。
from functools import lru_cache
@lru_cache()
def add(a, b):
print('calculate')
return a + b
def main():
print(add(1, 2))
print(add(1, 2))
if __name__ == '__main__':
main()
ポイントは add()
関数の中で calculate
という文字列を出力しているところ。
これで、実際に関数が呼び出されたのか、それともキャッシュされた値が返ったのか区別できる。
それでは、上記を保存して実行してみよう。
サンプルコードでは同じ引数 (1, 2)
を使って add()
関数を 2 回呼び出している。
$ python cache.py
calculate
3
3
実行しても calculate
が 1 回しか出力されない。
つまり 2 回目の呼び出しではキャッシュされた値が返っていることが分かる。
見事に @lru_cache
デコレータが機能しているようだ。
デコレータの本質
おさらいが終わったところで、早速本題に入る。
デコレータという機能は、実はシンタックスシュガー (糖衣構文) に過ぎない。
シンタックスシュガーというのは、プログラミング言語において、ある書き方に対して別の書き方ができるようにしたもの。
デコレータがシンタックスシュガーということは、つまり同じ内容はデコレータを使わなくても書けるということ。
先ほどのサンプルコードを、デコレータを使わない形に直してみよう。
つまり、足し算をする add()
関数をデコレータを使わずに functools.lru_cache
でメモ化している。
from functools import lru_cache
def add(a, b):
print('calculate')
return a + b
add = lru_cache()(add)
def main():
print(add(1, 2))
print(add(1, 2))
if __name__ == '__main__':
main()
上記を保存して実行してみよう。
$ python cache.py
calculate
3
3
ちゃんとメモ化が動作していることが分かる。
先ほどのサンプルコードでは functools.lru_cache
をデコレータとして使っていない。
代わりに、次のようなコードが登場している。
これは lru_cache()
を通して add()
関数を代入し直している。
ようするに add()
関数の内容が lru_cache()
の返り値で上書きされることになる。
add = lru_cache()(add)
つまり、最初のコードで登場した以下と上記は本質的に等価ということ。
@lru_cache()
def add(a, b):
これは理解する上で重要なポイントで、デコレータを使って書かれたコードは、必ず使わずに書くこともできる。
デコレータの作り方
続いてはデコレータの作り方を見ていく。
前述したように、デコレータは単なるシンタックスシュガーで、やっていることは単なる返り値を使った上書きだった。
それさえ分かっていればデコレータの作り方は理解しやすい。
例えば、関数をデコレートするデコレータについて考えてみよう。
これまで理解した内容から考えれば「関数を受け取って、代わりとなる関数を返す」ものを作れば良い。
以下のサンプルコードでは deco
という名前でデコレータを作っている。
見て分かる通り、普通の関数と見た目は何ら変わらない。
つまり deco
はデコレータとして動作する関数、ということになる。
デコレータとして動作するために、引数 func
という名前で関数を受け取って、代わりとなる wrapper()
という関数の参照を返している。
def deco(func):
"""デコレートした関数の前後に処理を挟み込む自作デコレータ"""
def wrapper(*args, **kwargs):
"""本来の関数の代わりに返される関数"""
print('before')
result = func(*args, **kwargs)
print('after')
return result
return wrapper
@deco
def greet():
"""文字列を書き出すだけの関数"""
print('Hello, World!')
def main():
greet()
if __name__ == '__main__':
main()
このデコレータは、本来の関数の呼び出しの前後に文字列の出力を挟み込むものになっている。
@deco
を使ってデコレートする対象は greet()
という関数で、内容は文字列を出力するだけ。
上記を保存して実行してみよう。
greet()
関数が出力する文字列の前後に @deco
で追加した処理が挟み込まれていることが分かる。
$ python deco.py
before
Hello, World!
after
念のため、デコレータを使わないパターンも見ておこう。
繰り返しになるけど、デコレータはただのシンタックスシュガーなので、必ず使わない形にも直せる。
デコレータを使わない形にすれば、やっていることがよく分かる。
def deco(func):
"""デコレートした関数の前後に処理を挟み込む自作デコレータ"""
def wrapper(*args, **kwargs):
"""本来の関数の代わりに呼び出される関数"""
print('before')
result = func(*args, **kwargs)
print('after')
return result
return wrapper
def greet():
"""文字列を書き出すだけの関数"""
print('Hello, World!')
greet = deco(greet)
def main():
greet()
if __name__ == '__main__':
main()
ようするに deco()
関数が greet()
関数の参照を受け取って、代わりに wrapper()
関数の参照を返しているだけ。
上記を保存して実行してみよう。
$ python deco.py
before
Hello, World!
after
ちゃんと動作している。
引数を受け取るデコレータ
先ほどのサンプルコードで登場した deco
デコレータは lru_cache
デコレータと違うところが一つあった。
それは、デコレータとして使うとき後ろにカッコがあるかないか。
lru_cache
の例を思い出すと、後ろにカッコがついていた。
@lru_cache()
def add(a, b):
それに対して deco
の例では、後ろにカッコがない。
@deco
def greet():
上記の違いは、デコレータが引数を受け取るか受け取らないか。
例えば lru_cache
であれば、キャッシュする数の上限を設定するために maxsize
というオプションがあったりするため。
つまり、こんな感じで書ける。
@lru_cache(maxsize=32)
def add(a, b):
先ほどの deco
を引数を受け取れるように書き換えてみよう。
次のサンプルコードでは deco
デコレータが本来の処理の前後に挿入するメッセージを引数で指定できるようにしている。
コード上の変化としては、先ほどよりも deco
のネストが増していることが分かる。
引数の受け取らないパターンで deco
という名前だった関数が今度は wrapper
という名前になって、新しい deco
がそれを返している。
def deco(before_msg='before', after_msg='after'):
"""引数を受け取るデコレータ (ネストが一段増える)"""
def wrapper(func):
def _wrapper(*args, **kwargs):
print(before_msg)
result = func(*args, **kwargs)
print(after_msg)
return result
return _wrapper
return wrapper
@deco('mae', 'ato')
def greet():
"""文字列を書き出すだけの関数"""
print('Hello, World!')
def main():
greet()
if __name__ == '__main__':
main()
上記を保存して実行してみよう。
今度はデコレータを使うときに指定した引数にもとづいて前後の出力が変化している。
$ python decoargs.py
mae
Hello, World!
ato
引数を取るパターンでは、取らないパターンよりも何をやっているのかが分かりにくいかもしれない。
これも、デコレータを使わない形に書き直すと理解しやすくなる。
以下のサンプルコードは、同じ内容をデコレータを使わない形に直してある。
def deco(before_msg='before', after_msg='after'):
"""引数を受け取るデコレータ (ネストが一段増える)"""
def wrapper(func):
def _wrapper(*args, **kwargs):
print(before_msg)
result = func(*args, **kwargs)
print(after_msg)
return result
return _wrapper
return wrapper
def greet():
"""文字列を書き出すだけの関数"""
print('Hello, World!')
greet = deco('mae', 'ato')(greet)
def main():
greet()
if __name__ == '__main__':
main()
上記を見ると、関数を上書きする工程が二段階に分かれていることが見て取れる。
冗長に分かりやすく書いたパターンでは、まず deco()
が関数を上書きするのに使う関数 (変数 wrap_func
) を返している。
そして、その関数を使って対象の関数 greet()
を上書きしている。
これが引数を受け取るデコレータの動作原理ということ。
以降は、デコレータを使わずに書いたパターンを示すことは基本的には省略する。
しかし、デコレータが単なるシンタックスシュガーで、使わないパターンに必ず書き直せるという点は意識しながら読むと理解が深まると思う。
デコレータの用途
デコレータの基本が分かったところで、次は用途について考えてみる。
デコレータの用途は、大きく分けて「ラッピング」と「マーキング」の二つがある。
これまで紹介してきた内容は、用途が全て前者の「ラッピング」だった。
ラッピング
それでは、まずラッピングの用途から見ていこう。
これは、これまでにも紹介してきた通り元の関数などをデコレータを通して上書きするというもの。
以下のサンプルコードでは関数の返り値に 2 倍をかけて返すデコレータ double
を定義している。
def double(func):
"""デコレートした関数の返り値を 2 倍にするデコレータ"""
def wrapper(*args, **kwargs):
return func(*args, **kwargs) * 2
return wrapper
@double
def add(a, b):
"""足し算をする関数"""
return a + b
def main():
print('1 + 2 =', add(1, 2))
if __name__ == '__main__':
main()
上記を保存して実行してみよう。
@double
デコレータによってデコレートされた add()
関数は、計算結果を倍にして返すように上書きされる。
$ python wrapping.py
1 + 2 = 6
ラッピング用途での注意点
ちなみに、ラッピング用途でデコレータを使うときは一つ注意点がある。
それは、ラッピング用途のデコレータが、デコレートしたオブジェクトを代わりの何かで上書きするという性質に由来している。
以下のサンプルコードを見てほしい。
このコードでは、デコレートされた関数 add()
の名前を __name__
プロパティから取得して出力している。
もちろん、本来の意図としては add
という文字列が出力されてほしいはず。
def double(func):
"""デコレートした関数の返り値を 2 倍にするデコレータ"""
def wrapper(*args, **kwargs):
return func(*args, **kwargs) * 2
return wrapper
@double
def add(a, b):
"""足し算をする関数"""
return a + b
def main():
print('add()\'s name:', add.__name__)
if __name__ == '__main__':
main()
上記を保存して実行してみよう。
$ python name.py
add()'s name: wrapper
なんと、残念ながら wrapper
という出力になってしまった。
ここまで読んできていれば、理由は何となく想像がつくと思う。
ようするにデコレータを通して add()
関数は wrapper()
関数に置き換えられてしまっている。
そのため add()
関数のつもりで扱うと、実際には置き換えられた関数だった、ということが起こる。
この問題は functools.wraps
を使うと解決できる。
以下のサンプルコードでは、デコレータが返す代わりの関数を functools.wraps
でデコレートしている。
from functools import wraps
def double(func):
"""デコレートした関数の返り値を 2 倍にするデコレータ"""
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs) * 2
return wrapper
@double
def add(a, b):
"""足し算をする関数"""
return a + b
def main():
print('add()\'s name:', add.__name__)
if __name__ == '__main__':
main()
上記を保存して実行してみよう。
今度はちゃんと add
という名前が出力された。
$ python name.py
add()'s name: add
このように functools.wraps
を使うと、置き換える関数が元の関数の性質を引き継げる。
マーキング
もう一つの用途としてマーキングを見てみよう。
この用途では、デコレータは受け取ったオブジェクトをそのまま返す。
ただし、受け取ったオブジェクトを何処かに記録しておいて、それを後から利用することになる。
以下のサンプルコードでは @register
デコレータでデコレートした関数は _MARKED_FUNCTIONS
というリストに保存される。
そして、保存されたリストから関数を呼び出している。
_MARKED_FUNCTIONS = []
def register(func):
"""関数を登録するデコレータ"""
_MARKED_FUNCTIONS.append(func)
return func
@register
def greet_morning():
print('Good morning!')
@register
def greet_afternoon():
print('Good afternoon!')
@register
def greet_evening():
print('Good evening!')
def main():
print(_MARKED_FUNCTIONS)
_MARKED_FUNCTIONS[0]()
if __name__ == '__main__':
main()
上記を保存して実行してみよう。
デコレートした関数がリストに保存されて、それを後から呼び出すことができている。
$ python marking.py
[<function greet_morning at 0x10b4d1598>, <function greet_afternoon at 0x10b59b378>, <function greet_evening at 0x10b59b400>]
Good morning!
マーキング用途のデコレータは、典型的にはイベントハンドラで用いられる。
例えば Web アプリケーションフレームワークの Flask は、マーキングした関数がクライアントからのアクセスを捌くハンドラになる。
もちろん、上記のコードもデコレータを使わない形に直せる。
_MARKED_FUNCTIONS = []
def register(func):
"""関数を登録するデコレータ"""
_MARKED_FUNCTIONS.append(func)
return func
def greet_morning():
print('Good morning!')
def greet_afternoon():
print('Good afternoon!')
def greet_evening():
print('Good evening!')
greet_morning = register(greet_morning)
greet_afternoon = register(greet_afternoon)
greet_evening = register(greet_evening)
def main():
print(_MARKED_FUNCTIONS)
_MARKED_FUNCTIONS[0]()
if __name__ == '__main__':
main()
上記を見て分かる通り、デコレータはモジュールが読み込まれるタイミングで解釈される。
そのため、あらかじめデコレートされた関数の情報を収集するようなこともできるというわけ。
関数以外で作るデコレータ
ここまで紹介してきたデコレータは、全て関数を使って実装されていた。
しかし、デコレータはそれ以外を使った作り方もある。
メソッドで作るデコレータ
例えば、以下のサンプルコードを見てほしい。
ここでは Decorator
クラスの deco()
というインスタンスメソッドでデコレータを実装している。
内容は最初に自作した処理の前後に出力を挿入するものだ。
class Decorator(object):
def deco(self, func):
"""デコレータとして機能するメソッド"""
def wrapper(*args, **kwargs):
print('before')
result = func(*args, **kwargs)
print('after')
return result
return wrapper
instance = Decorator()
@instance.deco
def greet():
print('Hello, World!')
def main():
greet()
if __name__ == '__main__':
main()
上記を保存して実行してみよう。
ちゃんとデコレータとして機能していることが分かる。
$ python instance.py
before
Hello, World!
after
インスタンスメソッドとしてデコレータを実装すると嬉しいのは、インスタンスごとにコンテキストを持たせられるところ。
以下のサンプルコードにおいて japanese
と english
という二つのインスタンスは、それぞれ異なる引数で初期化されている。
そして、それぞれが別の関数をデコレートしている。
class Decorator(object):
def __init__(self, before_msg='before', after_msg='after'):
self.before_msg = before_msg
self.after_msg = after_msg
def deco(self, func):
"""デコレータとして機能するメソッド"""
def wrapper(*args, **kwargs):
print(self.before_msg)
result = func(*args, **kwargs)
print(self.after_msg)
return result
return wrapper
japanese = Decorator('mae', 'ato')
english = Decorator('before', 'after')
@japanese.deco
def greet_morning():
print('Good morning')
@english.deco
def greet_afternoon():
print('Good afternoon')
def main():
greet_morning()
greet_afternoon()
if __name__ == '__main__':
main()
上記を保存して実行してみよう。
すると、初期化したときの引数によってデコレートされた結果が異なっていることが分かる。
$ python context.py
mae
Good morning
ato
before
Good afternoon
after
このようにインスタンスメソッドでデコレータを作ると、インスタンスにコンテキストをもたせられるというメリットがある。
呼び出し可能オブジェクトで作るデコレータ
メソッドで作るデコレータの変わり種として、呼び出し可能オブジェクトを使うパターンも考えられる。
これはクラスに特殊メソッド __call__()
を実装するというもの。
この特殊メソッドを実装すると、インスタンス自体を関数みたいに実行できるようになる。
で、その特殊メソッド __call__()
がデコレータとして動作するとしたら?という。
以下のサンプルコードでは特殊メソッド __call__()
がデコレータとして動作する。
そのためインスタンス化したオブジェクトの instance
が、そのままデコレータとして使えている。
class Decorator(object):
def __call__(self, func):
"""呼び出し可能オブジェクトを作るための特殊メソッド
デコレータとして動作する"""
def wrapper(*args, **kwargs):
print('before')
result = func(*args, **kwargs)
print('after')
return result
return wrapper
instance = Decorator()
@instance
def greet():
print('Hello, World!')
def main():
greet()
if __name__ == '__main__':
main()
実行結果はこれまでと変わらないので省略する。
デコレートする対象
ここまでの例では、デコレートする対象は全て関数だった。
しかし、デコレータは関数以外もデコレートすることができる。
メソッドをデコレートする
以下のサンプルコードでは、おなじみの @deco
デコレータがインスタンスメソッドをデコレートしている。
def deco(func):
"""デコレートした関数の前後に処理を挟み込む自作デコレータ"""
def wrapper(*args, **kwargs):
"""本来の関数の代わりに呼び出される関数"""
print('before')
result = func(*args, **kwargs)
print('after')
return result
return wrapper
class MyClass(object):
@deco
def greet(self):
print('Hello, World!')
def main():
obj = MyClass()
obj.greet()
if __name__ == '__main__':
main()
上記を保存して実行してみよう。
ちゃんと動くことが分かる。
$ python method.py
before
Hello, World!
after
ちなみにメソッドをデコレートするときの注意点が一つある。
それは、置き換える関数の第一引数にインスタンスオブジェクトを受け取れるようにすること。
Python のメソッドは、典型的には self
という名前で第一引数にインスタンスを受け取る。
置き換える関数が、この一つ余分な引数を受け取れるようになっていないと動作しない。
先ほどのサンプルコードでは引数を (*args, **kwargs)
という任意の形で受け取れるようにしていたので、特に気にすることはなかった。
クラスをデコレートする
デコレータはクラスをデコレートすることもできる。
以下のサンプルコードでは @deco
デコレータが MyClass
をデコレートしている。
@deco
デコレータでは、クラスが持っているメソッドを上書きして回っている。
上書きされたメソッドは、呼び出されたタイミングでその旨が出力されるようになる。
import inspect
def deco(cls):
"""クラスオブジェクトを引数に取るデコレータ
XXX: Python 3 でしか動作しない"""
methods = inspect.getmembers(cls, predicate=inspect.isfunction)
for method_name, method_object in methods:
wrapped_method = logging_wrapper(method_object)
setattr(cls, method_name, wrapped_method)
return cls
def logging_wrapper(func):
"""関数の呼び出しを記録するラッパー"""
def _wrapper(*args, **kwargs):
print('call:', func.__name__)
result = func(*args, **kwargs)
return result
return _wrapper
@deco
class MyClass(object):
def greet_morning(self):
print('Good morning!')
def greet_afternoon(self):
print('Good afternoon!')
def greet_evening(self):
print('Good evening!')
def main():
o = MyClass()
o.greet_morning()
o.greet_afternoon()
o.greet_evening()
if __name__ == '__main__':
main()
上記を保存して実行してみよう。
上書きされたメソッドによって、呼び出しが記録されていることが分かる。
$ python clsdeco.py
call: greet_morning
Good morning!
call: greet_afternoon
Good afternoon!
call: greet_evening
Good evening!
まとめ
今回扱った内容は以下の通り。
- デコレータの本質
- デコレータはシンタックスシュガー (糖衣構文) に過ぎない
- デコレータの作り方
- デコレータの用途
- デコレータの種類
- デコレータの対象
- デコレートできるのは関数、メソッド以外にクラスもある
上記さえ理解していれば、あとは目的に応じてどのようなデコレータを作れば良いかが自動的に決まる。
参考
www.python.org
www.python.org