読者です 読者をやめる 読者になる 読者になる

Pythonデコレータが熱い

Python

今回は
decorator 3.4.0 : Python Package Index
のお話。

Pythonのデコレータは便利です。関数の実体定義直後に、任意のフィルタをその関数に施すことができます。

def special(func):
    def invoker(*args, **kw):
        return "special " + func(*args, **kw)
    invoker.__name__ = func.__name__
    invoker.__dict__.update(func.__dict__)
    invoker.__doc__ = func.__doc__
    invoker.__module__ = func.__module__
    return invoker  #改変した関数を返す

@special
def myproc():
    return "hoge"

#ここで myproc = special(myproc) とするのと同じ

print myproc()
>> "special hoge"

うん、悪くない。けど、現実には、クロージャを意識してネストした関数を書くような実装を、ビジネスロジックを書く人(というか、書いているときの自分)に強制したくない。となると、デコレータをパラメータ化し、いちいち再実装しなくていいようにしたい。で、ありがちなのはこんなコード。

#パラメータ化されたデコレータの実装
def wrap_with(before_start, after_finish):
    def wrap_decorator(func):
        def invoker(*args, **kw):
            before_start(*args, **kw)
            r = func(*args, **kw)
            after_finish(*args, **kw)
            return r
        invoker.__name__ = func.__name__
        invoker.__dict__.update(func.__dict__)
        invoker.__doc__ = func.__doc__
        invoker.__module__ = func.__module__
        return invoker
    return wrap_decorator

#ここからユーザのコード
def before_start(*args, **kw):
    print "start"
def after_finish(*args, **kw):
    print "finish"

@wrap_with(before_start, after_finish)
def target_func():
    print "processing..."
target_func()

>> start
>> processing...
>> finish

3重の関数定義ネスト! 上手く動いてるうちはいいけど、バグ調査がデコレータの内部実装に及んだとき、これはきつい。コードレビューが激しく遅くなります。関数プログラムだから難しいんじゃ?とお思いの方、__call__メソッドをオーバーライドし、OOPで書いてみてもいいですよ。余計めんどくさくて、私は断念しました。
じっさい、TurboGearsのコントローラにくっつける例のやつには、こんなのがいっぱいあって、とてもじゃないけどデコレータの中まで追いかけきれません。

というわけで、decoratorモジュールです。
http://pypi.python.org/pypi/decorator/
http://www.phyast.pitt.edu/~micheles/python/documentation.html
これは、デコレータの実装をもっと簡潔にしようというもの。これで最初の例は、

from decorator import decorator

def special(func, *args, **kw):
    return "special " + func(*args, **kw)

@decorator(special)
def myproc():
    return "hoge"

print myproc()
>> "special hoge"

もしくは

from decorator import decorator

@decorator
def special(func, *args, **kw):
    return "special " + func(*args, **kw)

@special
def myproc():
    return "hoge"

print myproc()
>> "special hoge"

となります。前者は任意の関数をその場でデコレータ化して適用、後者はデコレータをあらかじめ作成しておく方法です。

パラメータ化デコレータの場合はこれ。

from decorator import decorator

def wrap_with(before_start, after_finish):
    def invoker(func, *args, **kw):
        before_start(*args, **kw)
        r = func(*args, **kw)
        after_finish(*args, **kw)
        return r
    return decorator(invoker)

ネストが2重で済みました。

OOPは解るけど、関数プログラムだとクロージャに慣れてなくて自信ないんだよね」という方には、こんな方法もありました。

class WrapDecorator(object):
    def __init__(self, before_start, after_finish):
        self.before_start = before_start
        self.after_finish = after_finish
    
    def call(self, func, *args, **kw):
        self.before_start(*args, **kw)
        r = func(*args, **kw)
        self.after_finish(*args, **kw)
        return r

wrap_with = decorator(WrapDecorator)

できあがったデコレータの使い方はまったく同じです。

def before_start(*args, **kw):
    print "start"
def after_finish(*args, **kw):
    print "finish"

@wrap_with(before_start, after_finish)
def target_func():
    print "processing..."
target_func()

>> start
>> processing...
>> finish

http://www.phyast.pitt.edu/~micheles/python/documentation.html
には、もっと面白い実用方法が掲載されていて、ためになるのでぜひ一度見てみることをお勧めします。メモ化(時間のかかる処理は一度出した答えを憶えておく)やJavaのsynchronizedメソッドをデコレータで実現する例では、こんなにコードが簡潔なまま、現実的な応用が可能なのかと感心することでしょう。