[python]デコレータでfunctools.wrap()を使う

前々回に書いた記事の続き。

functools.wrap()とは?

Pythonのマニュアルには

これはラッパ関数を定義するときに partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated) を関数デコレータとして呼び出す便宜関数です

と書かれているが正直サッパリ。
上記の説明文の下はサンプルコードが書かれていて、更にその下にはドキュメント文字列が失われると書いてある。

まあ要するにデコレータでラップされる側の関数が何かしら上書きされる、ということなんだけど公式のドキュメントにはその場合の例が書いてないので変更してみよう。
するとコードは以下のようになる。

def my_decorator(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()
print example.__name__
print example.__doc__

違いは単に@wrap(func)を取っただけ。
これを実行すると、以下のような表示がされる。

Calling decorated function
Called example function
wrapper
None

結果の3,4行目はmy_decoratorの中wrapperの関数の名前とドキュメンテーション文字列が返ってきている。
だから@wraps()を使ってやるとうまくいきますよーという事。

@wraps()無い場合の動きはどうなってるのか?

まずデコレータの仕組みとして流れをさっきのNGだったコードを使って説明。
コードはこれ。

def my_decorator(f):
    def wrapper(*args, **kwds):
        print 'Calling decorated function'
        return f(*args, **kwds)
    return wrapper

@my_decorator
def example():
    """Docstring"""
    print 'Called example function'

print example.__name__

流れとしては、

最後のprint文でexampleオブジェクトの解決をしようとする。

しかし@my_decoratorがあるので、まずmy_decoratorが呼び出される。この時のmy_decorator()の引数はexampleの関数オブジェクト。

wrapper()関数の定義が読まれる。

wraper()関数の関数オブジェクトが返される。(def my_decorator()内の最後にあるreturn wrapper)

戻ってきたオブジェクト(wrapper()関数)の関数名(__name__プロパティ)の読み込みをして表示。

というような感じで行くはず。たぶん。

何か問題でも?


@wrap()しなかった時の問題を自分なりに考えてみた結果、

  • help()とかdir()などをがデコレータの定義を返してしまう。
  • doctestがあった時にデコレータのdoctestが呼び出される。

かなーと思った。
doctestは特に大きいかもしれない。
その例を示す。

#-*- coding:utf-8 -*-

from functools import wraps

def my_decorator(f):
    def wrapper(*args, **kwds):
        return f(*args, **kwds)
    return wrapper


@my_decorator
def add(x, y):
    """
    2つの引数x, yを足し算した結果を返す。

        >>> add(1, 2)
        3

    """
    return x + y

こいつの意図としてはadd()関数のテストを行おうとしている。
これはverboseオプションをつけて実行すると

3 items had no tests:
    __main__
    __main__.add
    __main__.my_decorator
0 tests in 3 items.
0 passed and 0 failed.
Test passed.

と表示され、"0 tests in 3items." という表示からテスト自体は1つも行われていない事が分かる。
そこでこいつを@wraps()でラップしてやると、

Trying:
    add(1, 2)
Expecting:
    3
ok
2 items had no tests:
    __main__
    __main__.my_decorator
1 items passed all tests:
   1 tests in __main__.add
1 tests in 3 items.
1 passed and 0 failed.
Test passed.

となってテストが行われている事が分かる。
とまあ、こんなところ?

他こういう点でマズイですよってツッコミあったらくれるとうれしいです!

結論

デコレータはちゃんと@wraps()でつつんでやりましょうというお話でした。
次回、デコレータに変数を渡す場合の書き方!(まだデコレータネタで引っ張る人