Python functools.wraps()とは?デコレータで関数情報を保つ方法

Pythonのfunctools.wraps()は、デコレータで元の関数の情報を引き継ぐためのデコレータです。

付けないと関数名やドキュメントが消えてしまうため、デコレータを書くときの必須の作法とされています。


wrapsを付けないと何が起きるか

def deco(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@deco
def greet(name):
    """あいさつを返す"""
    return f"こんにちは、{name}さん"

print(greet.__name__)
print(greet.__doc__)

実行結果:

wrapper
None

名前がgreetではなくwrapperになり、docstringも消えました。

理由は単純です。@decoを付けるとgreet = deco(greet)が実行され、greetという名前にwrapper関数が入るからです。中身が入れ替わっているので、名前もdocstringもwrapperのものになります。


functools.wraps()で解決する

from functools import wraps

def deco(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@deco
def greet(name):
    """あいさつを返す"""
    return f"こんにちは、{name}さん"

print(greet.__name__)
print(greet.__doc__)

実行結果:

greet
あいさつを返す

内側のwrapperに@wraps(func)を付ける——これだけです。元の関数の情報がwrapperへコピーされます。


何がコピーされるのか

属性 内容
__name__ 関数名
__doc__ docstring
__module__ 定義されているモジュール
__qualname__ クラスを含む完全な名前
__dict__ 関数に付けられた属性
__wrapped__ 元の関数への参照(追加される)
print(greet.__module__, greet.__qualname__)
print(greet.__wrapped__.__name__)

実行結果:

__main__ greet
greet

コピーされた情報はinspectモジュールから確認できます。wrapsを付けないとsignature()が(*args, **kwargs)になってしまう点は「Python inspect.signature()とは?関数の引数を調べる方法」で解説しています。

__wrapped__で元の関数を呼べる

print(greet.__wrapped__("佐藤"))

実行結果:

こんにちは、佐藤さん

デコレータを通さずに元の処理だけ実行できます。テストで「ログ出力やリトライを外して本体だけ試したい」というときに便利です。


なぜ実務で問題になるのか

「名前が変わるだけ」と思うと軽視しがちですが、実際には次のような困り方をします。

  • デバッグが難しくなる:エラーを追っても全部wrapperと表示される
  • help()が役に立たなくなる:説明が出てこない
  • 自動ドキュメント生成が壊れる:Sphinxなどが関数名を拾えない
  • フレームワークが誤動作する:関数名でルーティングする仕組みだと、全部同じ名前になって衝突する

最後の項目が特に厄介です。FlaskやpytestのようにPythonの関数名を使って動くツールでは、デコレータを2つの関数に付けた時点で名前が衝突してエラーになります。


引数を取るデコレータでも同じ

from functools import wraps

def repeat(times):
    def deco(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return deco

@repeat(3)
def hello():
    """3回呼ばれる"""
    print("hello")

hello()
print(hello.__name__, "/", hello.__doc__)

実行結果:

hello
hello
hello
hello / 3回呼ばれる

入れ子が深くなっても、@wraps(func)を付ける場所は「実際に呼ばれる関数」の直前で変わりません。


update_wrapper()との関係

wraps()はupdate_wrapper()を使いやすくしたものです。

from functools import update_wrapper

def deco(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    update_wrapper(wrapper, func)
    return wrapper

やっていることは同じです。デコレータ構文で書けるぶんwraps()のほうが読みやすいため、通常はこちらを使います。


型ヒントまでは引き継がれない

wraps()は実行時の属性をコピーしますが、型チェッカーから見た引数の型までは正確に伝わりません。wrapper(*args, **kwargs)という定義がそのまま見えるためです。

厳密に型を保ちたい場合は、Python 3.10以降のtyping.ParamSpecを使います。日常的な用途ではwraps()だけで十分です。


functools.wraps()のまとめ

  • デコレータを書いたら内側の関数に必ず@wraps(func)
  • 付けないと__name__がwrapperになり、docstringが消える
  • 名前・docstring・モジュール・属性がコピーされる
  • __wrapped__から元の関数を直接呼べる
  • 付け忘れるとデバッグ・help()・フレームワークが壊れる
  • 引数付きデコレータでも付ける場所は同じ

あわせて読みたい

プログラミング学習に興味がある方は、まずは無料体験会へ

無料体験会・お申し込みはこちら