デコレータ
【 目次 】
デコレータを使って関数を別の関数でラップ(包み込む)する事で、関数の機能を拡張することができる。
デコレータの動作は単純で、ただ 関数 を 関数を引数にとる別の関数 で変換してもとの関数と同じ名前の関数に置き換える事である。
以下のコードは、デコレータ関数dec_funcを使ってfunc関数の機能を拡張して、関数の実行の前と実行後に文字列の表示を追加する例である。
いわゆるロギングの機能を、もとの関数に追加している。
def dec_func(func): def wrapper_func(*list_args,**dict_args): u"ラッパー関数" print "before", func.__name__ result=func(*list_args,**dict_args) print "after", func.__name__ return result return wrapper_func def func(): u"デコレートされる関数" print u"もとの関数を実行" func=dec_func(func) func()
実行結果
before func funcを実行 after func
wrapper_funcのなかでの関数funcの呼び出しのコードは、どのような引数を持つ関数でも処理できるようにfunc(*list_args,**dict_args)
を使って任意の引数を受け取る関数として実装している。
このプログラムでは、デコレータ関数dec_funcを使って、もとの関数funcをwrapper_funcに置き換える事で、元の関数の機能を拡張している。
見方を変えると、デコレータは関数をフックして新たな機能を付け加えるものという事ができる。
AOP(アスペクト指向プログラミング)にもとづいたプログラミングをいとも簡単に実装できるアイデアという見方もできる。
デコレータはクロージャの応用で、内側の関数wrapper_funcはクロージャそして,外側のデコレータ関数dec_funcはエンクロージャになっている。
デコレータのアイデア自体は単純な発想で、一部の他の言語でも応用可能。
ちなに、JavaScriptでのコードを以下に示す。
function dec_func(func){ return function(){ console.log("関数実行前"); func(); console.log("関数実行後"); }; } function func(){ console.log("もとの関数を実行"); } func=dec_func(func); func()
しかし、pythonならではの優れた点は@デコレータ関数というシンタックスシュガーが用意されている事。
上記のコードの関数funcの定義部分は以下のコードに置き換える事ができる。
@dec_func def func(): u"デコレートされる関数" print u"もとの関数を実行"
@dec_func
を関数定義の前に記述する事により自動的に
func=dec_func(func);
が実行される事になる。
と言葉をならべてみたが、なんとも説明しずらいので、他のサイトの参考記事をしめしておく。
functools.wraps
デコレータを使うと、もともとの関数のドキュメンテーション文字列や関数名は失われてしまい、ラッパー側の関数名や文字列に置き換えられてしまう。
print func.__name__ print func.__doc__
実行結果
wrapper_func ラッパー関数
functoolsモジュールのwraps関数を使うと、これを防ぐ事ができる。
import functools def dec_func(func): def wrapper_func(*list_args,**dict_args): u"ラッパー関数" print "before", func.__name__ result=func(*list_args,**dict_args) print "after", func.__name__ return result return functools.wraps(func)(wrapper_func)
もしくは、functools.wraps関数をwrapper_func関数のデコレータとして使って、
def dec_func(func): @functools.wraps(func) def wrapper_func(*list_args,**dict_args): u"ラッパー関数" print "before", func.__name__ result=func(*list_args,**dict_args) print "after", func.__name__ return result return wrapper_func
ドキュメンテーション文字列や関数名を表示してみると
print func.__name__ print func.__doc__
実行結果
func デコレートされる関数
引数を持つデコレータ
引数を持つデコレータを定義する事ができる。
@dec_func_with_arg("xxx") def func(): print "もとの関数を実行"
これは、以下のコードと等価
func=dec_func_with_arg("xxx")(func)
引数を持つデコレータはやや難解。
う~ん!どのようなデコレータ関数を実装すればいいのだろうか、と悩んでしまったが。
引数付きの関数を用意して、その戻り値として引数なしのデコレータ関数を返せば良いようだ。
つまり、外側に引数を持つ関数をもう1つかぶせてデコレータ関数を返す関数を実装。
前述のdec_funcの外側にもう1つ関数をかぶせて
def dec_func_with_arg(arg): def dec_func(func): def wrapper_func(*list_args,**dict_args): print "before", func.__name__ result=func(*list_args,**dict_args) print "after", func.__name__ return result return wrapper_func return dec_func
ここでは、引数argは何の役割もはたしていないが。
では、デコレータの引数がどのように使えるかというと
次のコードは、指定の回数分、もとの関数を繰り返し実行する例である。
import functools def dec_multiple_times_func(arg_times): def dec_func(func): def wrapper_func(*list_args, **dict_args): for i in xrange(arg_times) : result = func(*list_args, **dict_args) return result return functools.wraps(func)(wrapper_func) return dec_func @dec_multiple_times_func(3) def func(): print u"もとの関数を実行" func()
実行結果
もとの関数を実行 もとの関数を実行 もとの関数を実行
もう1つ、引数付きのデコレータの例を。
以下のコードは引数を複数指定して、もとの関数の実行前と後にそれぞれ別々の関数を実行する例である。
import functools def dec_func_with_arg(befor_func, after_func): def dec_func(func): def wrapper_func(*list_args, **dict_args): befor_func() result = func(*list_args, **dict_args) after_func() return result return functools.wraps(func)(wrapper_func) return dec_func def before_func(): print u"before_funcを実行" def after_func(): print u"after_funcを実行" @dec_func_with_arg(before_func, after_func) def func(): print u"もとの関数を実行" func()
実行結果
before_funcを実行 もとの関数を実行 after_funcを実行
デコレータの引数を利用して、関数の実行前の処理と実行後の処理を、デコレートされる側の関数に対して個別に指定できることになり、よりキメの細かい処理が可能となる。
クラスによるデコレータの実装
クラスのインスタンスやメソッドを利用して、デコレータ関数を実装する事ができる。
クラスの属性やインスタンスの属性などを利用してクラスとしての機能を活用する事により、より高度なデコレータの実装の可能性が広がってくる。
callableなオブジェクトをデコレータとして使う。
クラスに特殊メソッド__call__を実装する事で、クラスのインスタンスをcallable(呼び出し可能)なオブジェクトとして実装する事ができる。
これを利用してクラスのインスタンスをデコレータとして使う事ができる。
import functools class DecCls: def __call__(self, func): @functools.wraps(func) def wrapper_func(*list_args,**dict_args): u"ラッパー関数" print "before", func.__name__ result=func(*list_args,**dict_args) print "after", func.__name__ return result return wrapper_func dec_func=DecCls() @dec_func def func(): u"デコレートされる関数" print u"もとの関数を実行" func()
実行結果
before func もとの関数を実行 after func
メソッドをデコレータとして使う。
クラスに特殊メソッド__call__を実装するのでは無く、単純にデコレータ関数としてクラスのメソッドを利用する事もできる。
以下はインスタンスメソッドをデコレータ関数として使う例である。
import functools class DecCls: def method(self, func): @functools.wraps(func) def wrapper_func(*list_args,**dict_args): u"ラッパー関数" print "before", func.__name__ result=func(*list_args,**dict_args) print "after", func.__name__ return result return wrapper_func dec_func=DecCls() @dec_func.method def func(): u"デコレートされる関数" print u"もとの関数を実行"
ここではインスタンスメソッドを使ってデコレータ関数を実装したが、クラスメソッドやスタティックメソドも利用可能。
複数のデコレータを指定
デコレータを複数指定する事も可能で、その場合には関数の定義の近くに指定されたデコレータから順番にデコレートされる事になる。
@outer_deco_func @inner_deco_func def func(): pass
は、以下のコードと等価になる。
func=outer_deco_func(inner_deco_func(func))
前述のdec_func関数を複数指定すると
@dec_func @dec_func def func(): print u"もとの関数を実行" func()
実行結果
before wrapper_func before func もとの関数を実行 after func after wrapper_func
関数の実行前と後に、2重に関数がフックされている事がわかる。
その他の参考記事。
- Pythonのデコレータを理解するための12Step - Qiita
デコレータ以外にも関数を扱うえで参考になる部分が