Python の特徴的な構文の一つにデコレータがある。 便利な機能なんだけど、最初はとっつきにくいかもしれない。 そこで、今回はデコレータについて一通り色々と書いてみる。 先に断っておくと、とても長い。
これを読むと、以下が分かる。
- デコレータの本質
- デコレータはシンタックスシュガー (糖衣構文) に過ぎない
- デコレータの作り方
- 引数を取るデコレータと取らないデコレータ
- デコレータの用途
- 用途はラッピングとマーキングの二つに大別できる
- デコレータの種類
- デコレータは関数、メソッド、インスタンスで作れる
- デコレータの対象
- デコレートできるのは関数、メソッド以外にクラスもある
今回使った環境は次の通り。 尚、紹介するコードの中には、一部に Python 3 以降でないと動作しないものが含まれている。
$ python -V Python 3.6.6
デコレータについて
まずはデコレータのおさらかいから。
デコレータは、その名の通りオブジェクトをデコレーション (装飾) するための機能。
構文としては、デコレートしたいオブジェクトの前で @
を先頭につけて使う。
デコレートできるオブジェクトの種類は、関数、メソッド、クラスがサポートされている。
標準モジュールにも、組み込みでいくつかのデコレータがある。
その中の一つを見てみよう。
以下のサンプルコードでは functools
モジュールの lru_cache
というデコレータを使っている。
このデコレータを使うと、デコレートした関数を簡単にメモ化できる。
メモ化というのは、ようするに関数の戻り値をキャッシュすること。
サンプルコードでは足し算をする add()
という関数をメモ化している。
#!/usr/bin/env python # -*- coding: utf-8 -*- from functools import lru_cache # 関数をメモ化するデコレータ @lru_cache() def add(a, b): # 実際に関数が処理された場合を区別するための出力 print('calculate') return a + b def main(): # 同じ引数で 2 回呼び出す 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
でメモ化している。
#!/usr/bin/env python # -*- coding: utf-8 -*- from functools import lru_cache # デコレータ構文は使っていない def add(a, b): print('calculate') return a + b # デコレータの代わりになる書き方 # デコレータ構文は、以下を書きやすくしたシンタックスシュガーにすぎない add = lru_cache()(add) def main(): # 同じ引数で 2 回呼び出す 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()
という関数の参照を返している。
#!/usr/bin/env python # -*- coding: utf-8 -*- def deco(func): """デコレートした関数の前後に処理を挟み込む自作デコレータ""" def wrapper(*args, **kwargs): """本来の関数の代わりに返される関数""" print('before') # 本来の関数が呼び出される前に実行される処理 result = func(*args, **kwargs) # 本来の関数の呼び出し print('after') # 本来の関数が呼び出された後に実行される処理 return result # 本来の関数の返り値を返す # 引数で関数を受け取って、代わりに別の関数を返す return wrapper # @deco デコレータで greet() 関数をデコレートしている @deco def greet(): """文字列を書き出すだけの関数""" print('Hello, World!') def main(): # デコレータでデコレートされた関数を呼び出す greet() if __name__ == '__main__': main()
このデコレータは、本来の関数の呼び出しの前後に文字列の出力を挟み込むものになっている。
@deco
を使ってデコレートする対象は greet()
という関数で、内容は文字列を出力するだけ。
上記を保存して実行してみよう。
greet()
関数が出力する文字列の前後に @deco
で追加した処理が挟み込まれていることが分かる。
$ python deco.py before Hello, World! after
念のため、デコレータを使わないパターンも見ておこう。 繰り返しになるけど、デコレータはただのシンタックスシュガーなので、必ず使わない形にも直せる。 デコレータを使わない形にすれば、やっていることがよく分かる。
#!/usr/bin/env python # -*- coding: utf-8 -*- 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
がそれを返している。
#!/usr/bin/env python # -*- coding: utf-8 -*- 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
引数を取るパターンでは、取らないパターンよりも何をやっているのかが分かりにくいかもしれない。 これも、デコレータを使わない形に書き直すと理解しやすくなる。 以下のサンプルコードは、同じ内容をデコレータを使わない形に直してある。
#!/usr/bin/env python # -*- coding: utf-8 -*- 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) # より冗長に、分かりやすく書くと以下のようになる # wrap_func = deco('mae', 'ato') # greet = wrap_func(greet) def main(): greet() if __name__ == '__main__': main()
上記を見ると、関数を上書きする工程が二段階に分かれていることが見て取れる。
冗長に分かりやすく書いたパターンでは、まず deco()
が関数を上書きするのに使う関数 (変数 wrap_func
) を返している。
そして、その関数を使って対象の関数 greet()
を上書きしている。
これが引数を受け取るデコレータの動作原理ということ。
以降は、デコレータを使わずに書いたパターンを示すことは基本的には省略する。 しかし、デコレータが単なるシンタックスシュガーで、使わないパターンに必ず書き直せるという点は意識しながら読むと理解が深まると思う。
デコレータの用途
デコレータの基本が分かったところで、次は用途について考えてみる。 デコレータの用途は、大きく分けて「ラッピング」と「マーキング」の二つがある。 これまで紹介してきた内容は、用途が全て前者の「ラッピング」だった。
ラッピング
それでは、まずラッピングの用途から見ていこう。
これは、これまでにも紹介してきた通り元の関数などをデコレータを通して上書きするというもの。
以下のサンプルコードでは関数の返り値に 2 倍をかけて返すデコレータ double
を定義している。
#!/usr/bin/env python # -*- coding: utf-8 -*- def double(func): """デコレートした関数の返り値を 2 倍にするデコレータ""" def wrapper(*args, **kwargs): # 本来の関数の返り値に 2 をかけて返す return func(*args, **kwargs) * 2 return wrapper # 返り値を倍にするデコレータをつける @double def add(a, b): """足し算をする関数""" return a + b def main(): # 1 + 2 を計算すると...? print('1 + 2 =', add(1, 2)) # 1 + 2 = 3 ... 6!! if __name__ == '__main__': main()
上記を保存して実行してみよう。
@double
デコレータによってデコレートされた add()
関数は、計算結果を倍にして返すように上書きされる。
$ python wrapping.py 1 + 2 = 6
ラッピング用途での注意点
ちなみに、ラッピング用途でデコレータを使うときは一つ注意点がある。 それは、ラッピング用途のデコレータが、デコレートしたオブジェクトを代わりの何かで上書きするという性質に由来している。
以下のサンプルコードを見てほしい。
このコードでは、デコレートされた関数 add()
の名前を __name__
プロパティから取得して出力している。
もちろん、本来の意図としては add
という文字列が出力されてほしいはず。
#!/usr/bin/env python # -*- coding: utf-8 -*- def double(func): """デコレートした関数の返り値を 2 倍にするデコレータ""" def wrapper(*args, **kwargs): # 本来の関数の返り値に 2 をかけて返す return func(*args, **kwargs) * 2 return wrapper # 返り値を倍にするデコレータをつける @double def add(a, b): """足し算をする関数""" return a + b def main(): # add() 関数の名前は? 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
でデコレートしている。
#!/usr/bin/env python # -*- coding: utf-8 -*- from functools import wraps def double(func): """デコレートした関数の返り値を 2 倍にするデコレータ""" # デコレータが返す関数を functools.wraps でデコレートする @wraps(func) def wrapper(*args, **kwargs): # 本来の関数の返り値に 2 をかけて返す return func(*args, **kwargs) * 2 return wrapper # 返り値を倍にするデコレータをつける @double def add(a, b): """足し算をする関数""" return a + b def main(): # add() 関数の名前は? print('add()\'s name:', add.__name__) if __name__ == '__main__': main()
上記を保存して実行してみよう。
今度はちゃんと add
という名前が出力された。
$ python name.py
add()'s name: add
このように functools.wraps
を使うと、置き換える関数が元の関数の性質を引き継げる。
マーキング
もう一つの用途としてマーキングを見てみよう。
この用途では、デコレータは受け取ったオブジェクトをそのまま返す。
ただし、受け取ったオブジェクトを何処かに記録しておいて、それを後から利用することになる。
以下のサンプルコードでは @register
デコレータでデコレートした関数は _MARKED_FUNCTIONS
というリストに保存される。
そして、保存されたリストから関数を呼び出している。
#!/usr/bin/env python # -*- coding: utf-8 -*- # デコレータでデコレートした関数を入れるリスト _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 は、マーキングした関数がクライアントからのアクセスを捌くハンドラになる。
もちろん、上記のコードもデコレータを使わない形に直せる。
#!/usr/bin/env python # -*- coding: utf-8 -*- # デコレータでデコレートした関数を入れるリスト _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()
というインスタンスメソッドでデコレータを実装している。
内容は最初に自作した処理の前後に出力を挿入するものだ。
#!/usr/bin/env python # -*- coding: utf-8 -*- 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
という二つのインスタンスは、それぞれ異なる引数で初期化されている。
そして、それぞれが別の関数をデコレートしている。
#!/usr/bin/env python # -*- coding: utf-8 -*- 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
が、そのままデコレータとして使えている。
#!/usr/bin/env python # -*- coding: utf-8 -*- 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
デコレータがインスタンスメソッドをデコレートしている。
#!/usr/bin/env python # -*- coding: utf-8 -*- 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
デコレータでは、クラスが持っているメソッドを上書きして回っている。
上書きされたメソッドは、呼び出されたタイミングでその旨が出力されるようになる。
#!/usr/bin/env python # -*- coding: utf-8 -*- 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): # 本当は logging モジュールを使うべき 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!
まとめ
今回扱った内容は以下の通り。
- デコレータの本質
- デコレータはシンタックスシュガー (糖衣構文) に過ぎない
- デコレータの作り方
- 引数を取るデコレータと取らないデコレータ
- デコレータの用途
- 用途はラッピングとマーキングの二つに大別できる
- デコレータの種類
- デコレータは関数、メソッド、インスタンスで作れる
- デコレータの対象
- デコレートできるのは関数、メソッド以外にクラスもある
上記さえ理解していれば、あとは目的に応じてどのようなデコレータを作れば良いかが自動的に決まる。
参考
スマートPythonプログラミング: Pythonのより良い書き方を学ぶ
- 作者: もみじあめ
- 発売日: 2016/03/12
- メディア: Kindle版
- この商品を含むブログ (1件) を見る