Python

Pythonのデコレータにはwrapsをつけるべきという覚え書き

More than 1 year has passed since last update.

Pythonのデコレータは最初理解するのが難しい。
そして理解したと思っていても実はベストプラクティスな実装ではないという事がしばしばある。
そんな中私が今まで知らなかった functools.wraps とは。

公式ドキュメント

https://docs.python.jp/3/library/functools.html#functools.wraps

@functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)(原文)

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

Why?
これだけみるとなんのこっちゃですが、ようするに
関数の名前やDocstringがデコレータになっちゃうよ
ということかな

実際に動きを見る

テストコード

def hoge_decorator(f):
    def hoge_wrapper(*args, **kwargs):
        """デコレータのDocstringだよ"""
        print("デコレータだよ")
        return f(*args, **kwargs)
    return hoge_wrapper

@hoge_decorator
def hoge_function():
    """デコってる関数のDocstringだよ"""
    print("これがデコってる関数だ!")

if __name__ == '__main__':
    hoge_function()

結果

デコレータだよ
これがデコってる関数だ!

よく見るやつ。
動きは特に問題ない。

だが・・・

def hoge_decorator(f):
    def hoge_wrapper(*args, **kwargs):
        """デコレータのDocstringだよ"""
        print("デコレータだよ")
        return f(*args, **kwargs)
    return hoge_wrapper

@hoge_decorator
def hoge_function():
    """デコってる関数のDocstringだよ"""
    print("これがデコってる関数だ!")

if __name__ == '__main__':
    hoge_function()
    print(hoge_function.__name__)
    print(hoge_function.__doc__)

結果

デコレータだよ
これがデコってる関数だ!
hoge_wrapper
デコレータのDocstringだよ

あれれー?おっかしいぞー?
printで出力された隠し属性がデコレータになっていますね。

これで何が問題かというと、doctestが動かなくなります。(unittestの人も多いと思いますが…)
他にも、通常動作は問題ないかもだけど名称とかが実は動かしている関数の名前じゃないという、
何を言ってるのかわからないが…的な展開になります。

functools.wraps

そこで
functools.wrapsの出番です。
早速使ってみましょう。

import functools

def hoge_decorator(f):
    @functools.wraps(f)
    def hoge_wrapper(*args, **kwargs):
        """デコレータのDocstringだよ"""
        print("デコレータだよ")
        return f(*args, **kwargs)
    return hoge_wrapper

@hoge_decorator
def hoge_function():
    """デコってる関数のDocstringだよ"""
    print("これがデコってる関数だ!")

if __name__ == '__main__':
    hoge_function()
    print(hoge_function.__name__)
    print(hoge_function.__doc__)

結果

デコレータだよ
これがデコってる関数だ!
hoge_function
デコってる関数のDocstringだよ

いいですね。

doctest

最後はdoctest。
ちなみに通常だとテストをしない場合とテストにパスした場合はなにもログがでないので -v をパラメータとしてつけます。
また、デコレータのテストも走るのでデコレータのDocstringは削除してあります。

  • wraps無し
import functools
import doctest

def hoge_decorator(f):
    def hoge_wrapper(*args, **kwargs):
        return f(*args, **kwargs)
    return hoge_wrapper

@hoge_decorator
def hoge_function(n):
    """デコってる関数のDocstringだよ
    >>> hoge_function(2)
    4
    """
    return n ** 2
if __name__ == '__main__':
    doctest.testmod()

結果

3 items had no tests:
    __main__
    __main__.hoge_decorator
    __main__.hoge_function
0 tests in 3 items.
0 passed and 0 failed.
Test passed.

やはりテストが走ってないですね。

  • wraps有り
import functools
import doctest

def hoge_decorator(f):
    @functools.wraps(f)
    def hoge_wrapper(*args, **kwargs):
        return f(*args, **kwargs)
    return hoge_wrapper

@hoge_decorator
def hoge_function(n):
    """デコってる関数のDocstringだよ
    >>> hoge_function(2)
    4
    """
    return n ** 2
if __name__ == '__main__':
    doctest.testmod()

結果

Trying:
    hoge_function(2)
Expecting:
    4
ok
2 items had no tests:
    __main__
    __main__.hoge_decorator
1 items passed all tests:
   1 tests in __main__.hoge_function
1 tests in 3 items.
1 passed and 0 failed.
Test passed.

ちゃんとテストが通りました。