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()・フレームワークが壊れる - 引数付きデコレータでも付ける場所は同じ



