3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python】デコレータで失われる関数情報を守ろう:`functools.wraps`

Posted at

Pythonのデコレータについて少し調べていたら、メタデータが失われる問題に気づきました🤔 functools.wrapsを使うと関数名やdocstringが保持されるみたい。自分用の備忘録として記事にまとめてみました。デバッグやドキュメント生成時に役立つかもしれません。型注釈も守られるのは知らなかった…。誰かの参考になれば嬉しいです🙏

はじめに

image.png

Pythonを使って開発をしていると、「デコレータ」という便利な機能を目にすることがあるかと思います。デコレータは関数に新しい機能を追加(修飾)できる強力なツールですが、そのまま使うと思わぬ落とし穴にはまることがあります。

例えば、

  • デコレータのせいで元の関数名やドキュメント文字列(docstring)が上書きされてしまう
  • デバッグ時に「どの関数を呼び出しているか分からない」状態に陥る

といった問題です。そこで、本記事では「functools.wraps」を使った解決方法を紹介します。これを知っておくと、デバッグやドキュメント作成がぐっと楽になりますよ。


デコレータって何?

image.png

まずはデコレータの基本を簡単におさらいします。

  • デコレータとは
    関数やメソッドを「ラップ」して、新たな機能を追加する仕組みです。引数の前後でログを出したり、実行時間を計測したり、何か特別な処理を挟みたいときなどにとても便利です。

  • 基本的な書き方

    def デコレータ(func):
        def wrapper(*args, **kwargs):
            # 何らかの前処理
            result = func(*args, **kwargs)
            # 何らかの後処理
            return result
        return wrapper
    
  • 使い方

    @デコレータ
    def 任意の関数(...):
        ...
    

例:挨拶を拡張する

簡単な例を示します。関数が呼び出される前後に「励ましのメッセージ」を付与してくれるデコレータを作成してみましょう。

def 挨拶を飾る(func):
    def wrapper(名前):
        print("素敵な一日になりますように!")
        func(名前)
        print("今日も頑張りましょう!")
    return wrapper

@挨拶を飾る
def こんにちは(名前):
    """この関数は名前を受け取って挨拶します"""
    print(f"こんにちは、{名前}さん!")

こんにちは("田中")

実行結果:

素敵な一日になりますように!
こんにちは、田中さん!
今日も頑張りましょう!

デコレータの落とし穴:元の関数情報が失われる

便利なデコレータですが、注意しないと元の関数のメタ情報(関数名やdocstring)が書き換えられてしまいます。先ほどの例で検証してみましょう。

print(こんにちは.__name__)  # 出力: wrapper
print(こんにちは.__doc__)   # 出力: None
  • こんにちは.__name__ が本来は こんにちは であるはずなのに wrapper になってしまう
  • こんにちは.__doc__(本来のdocstring)も消えて None になっている

ドキュメント生成ツールで関数の説明を自動的に拾えなくなったり、デバッグ時のログで正しい関数名が出なくなったりして困ります。


解決策:functools.wrapsの使用

この問題は標準ライブラリの functools に用意されている wraps デコレータを使うだけで、あっさりと解決できます。

使い方

from functools import wraps

def 挨拶を飾る(func):
    @wraps(func)  # ← これを付けるだけでOK!
    def wrapper(名前):
        print("素敵な一日になりますように!")
        func(名前)
        print("今日も頑張りましょう!")
    return wrapper

@挨拶を飾る
def こんにちは(名前):
    """この関数は名前を受け取って挨拶します"""
    print(f"こんにちは、{名前}さん!")

print(こんにちは.__name__)  # こんにちは
print(こんにちは.__doc__)   # この関数は名前を受け取って挨拶します

ご覧の通り、こんにちは.__name__こんにちは.__doc__も正しく元の関数の情報を保持できるようになりました。


さらに一歩進んだ使い方:引数や戻り値の型情報を守る

image.png

最近ではPythonで型注釈(type hint)を使う人も多いと思います。wrapsを使うと、以下のような情報もきちんとラップしてくれます。

  • 引数や戻り値に付与した型アノテーション
  • 関数のモジュール情報など

ドキュメント生成ツール(Sphinxやpdocなど)でも活用されることが多いので、型をきちんと書いている人は特にwrapsの利用が大切になってきます。


まとめ

image.png

  • デコレータは、既存の関数に機能を追加するための便利な仕組み
  • ただしそのまま使うと、元の関数の名前やdocstringなどの大事なメタ情報が失われる
  • functools.wrapsを使うだけで、この問題を簡単に解消できる
  • デバッグやドキュメント生成時の混乱を防ぐためにも、デコレータを書くときは @wraps(func) を忘れないようにしよう

参考資料


以上が、デコレータで失われがちなメタ情報を守りながら活用する方法でした。ちょっとしたテクニックですが、コードの可読性やメンテナンス性を格段に上げてくれるので、ぜひ活用してみてください。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?