デコレータとは「関数を包む関数」のこと
こう書くと抽象的に聞こえますが、仕組み自体はシンプルです。python のデコレータは、関数を受け取って関数を返すだけ。返される関数の中で元の関数を呼び出しつつ、その前後に何らかの処理を追加する、というのが基本の形です。
いちばん短い例で見てみましょう。
shout がデコレータの本体です。関数(ここでは greet)を受け取り、元の関数を呼び出して結果を大文字にする新しい関数(wrapper)を組み立てて返します。greet = shout(greet) と代入し直すことで、元の関数がラップされたバージョンに差し替わるわけです。
この「代入し直し」のパターンがあまりに頻出するので、Python は専用の構文を用意しました。
@ は代入の糖衣構文
def の直前の行に書く @名前 は、関数定義の直後に 名前 = 名前(...) と書くのと同じ意味になります。
@shout は「この関数に shout デコレータを適用する」という意味で読みます。Python は def の直後に greet = shout(greet) を実行しているだけで、仕組みは先ほどと同じ。単にタイプ量が減るというわけです。
@name を見かけたら、頭の中で function = name(function) に置き換えてみてください。構文の正体はそれだけです。
引数を受け取るデコレータ
実際の関数はたいてい引数を取ります。使い物になるデコレータなら、その引数をそのまま渡してあげる必要があります。定番のイディオムが *args, **kwargs ― 任意の引数を受け取るための Python の書き方ですね。ラッパー側は、中の関数がどんな引数を期待しているか気にする必要がないからです。
*args で位置引数をまとめて受け取り、**kwargs でキーワード引数をまとめて受け取ります。ラッパー関数は受け取った引数をそのまま元の関数に渡し、そのうえでデコレータ本来の処理を加えます。ここでは、戻り値を大文字に変換していますね。
実際に使われている python デコレータは、だいたいこの形をしています。
実用的な例: 実行時間を計測するデコレータ
関数の実行時間を表示してみましょう。
「呼び出しの前に何かをやって、呼び出しの後にも何かをやる」——このパターンは、デコレータを書いていると結局ほとんどのケースで登場します。ロギング、認証チェック、リトライ処理、入力バリデーション、どれも同じ形に落ち着くんです。
元の関数の情報を残す:functools.wraps の使い方
関数をデコレートするということは、実質的にその関数を別の関数で置き換えることです。そのため、ラップされた側の関数は本来持っていた __name__ や __doc__ といった属性を失ってしまいます。
greet.__name__ は "wrapper" になってしまい、docstring も消えています。これでは help() やトレースバック、関数情報を参照するツール類がすべて壊れてしまいます。
対処は一行だけ。内側の関数に @functools.wraps(func) を付けておけば、メタデータをまとめてコピーしてくれます。
@wraps(func) は内側の関数に必ず付けておきましょう。コストはゼロなのに、後々のデバッグで「あれ?」となるのを防いでくれます。
引数を受け取るデコレータ
デコレータ自体に設定値を渡したい場面もあります。たとえば「この関数を最大3回までリトライする」「ログレベルを DEBUG で出力する」といったケースですね。こうしたときは、もう1段ネストを深くします。つまり、引数を受け取ってデコレータを返す外側の関数を用意する、という構造です。
3層と聞くと一見複雑そうですが、外側から順に読んでいけばシンプルです。
repeat(times=3)は関数呼び出しで、戻り値はdecoratorです。decoratorが本物のデコレータで、関数を受け取ってラップした関数を返します。wrapperがラップ後の関数で、呼び出されたタイミングで実行されます。
この構造は @retry(times=5) や @cache(maxsize=100)、さらに @app.route("/users") のようなフレームワークのデコレータでも同じです。一度この3層パターンが見えるようになれば、同じ仲間のデコレータはすべて同じように読み解けます。
python デコレータを複数重ねる
1つの関数に複数のデコレータを重ねて適用することもできます。適用順は 下から上 で、def に一番近いものが最初に実行されます。
まず add_exclaim が先に適用されて ! を付け、続いて shout がそれを包み込んですべて大文字にします。結果は HI, ROSA! になります。
順番は大事です。スタックの順序を入れ替えると、大文字化した 後 に ! が付く形になります。今回は見た目が同じになるので違いが分かりにくいですが、たとえば JSON を整形するデコレータと入力をログに出すデコレータを組み合わせた場合、どちらを先に適用するかで結果はまるで変わってきます。
よく見かる組み込みデコレータ
Python 本体や標準ライブラリには、実コードで頻繁に目にするデコレータがいくつか用意されています。
@propertyを使うと、メソッドを属性のように参照できる「計算属性」に変えられます。@staticmethodはselfもclsも受け取らないメソッドであることを示す印です。@classmethodはインスタンスではなくクラス自身をclsとして受け取るので、別コンストラクタを用意したいときに重宝します。@functools.lru_cacheは結果をメモ化してくれるデコレータで、同じ引数で呼び出されたときはキャッシュから返してくれます。
フレームワークでよく見かけるデコレータ(@app.route、@pytest.fixture、@dataclass など)も、中身はまったく同じ仕組みです。特別な魔法があるわけではなく、関数をラップする関数にすぎません。
デコレータを書くべきとき、書かないべきとき
同じ「ふるまい」を複数の関数に共通して適用したいとき、たとえば処理時間の計測、ログ出力、リトライ、権限チェックなどは、デコレータの出番です。ポイントは、そのふるまいを関数本体から切り離しておけること。
逆に、次のような場合はデコレータを避けた方がいいです。
- そのふるまいが特定の 1 つの関数にしか必要ない。だったら関数内に直接書いた方が素直です。
- テストのためだけに使いたい。それならフィクスチャや引数で渡す方がわかりやすいです。
- 4 つも 5 つも重ねたくなってきた。そうなると実際に何が実行されているのかがデコレータの連鎖に隠れてしまい、読む人は一枚ずつ皮をむかないと処理の流れを追えません。普通のヘルパー関数にまとめた方が、ずっと読みやすくなるケースも多いです。
デコレータは切れ味の鋭い道具です。うまく使えば DRY に保てて意図もクリアになりますが、使い方を誤るとプログラムの挙動を覆い隠してしまいます。迷ったときは「読んでわかる方」を選ぶのが無難です。
次は型ヒント
実は型ヒントが登場する典型的な場面のひとつがデコレータです。wrapper 関数のシグネチャにアノテーションを付けることが多いからですね。型ヒントは小さな機能ですが効果は大きいもの。次回はそれを見ていきましょう。
よくある質問
Pythonのデコレータとは何ですか?
デコレータは「関数を受け取って、新しい関数を返す関数」のことです。多くの場合、元の関数に処理を追加した別の関数を返します。使うときは def の上の行に @デコレータ名 と書くだけ。この @ 記法は、実は func = デコレータ名(func) の省略形にすぎません。
デコレータは何に使うんですか?
関数の中身を書き換えずに、周りに処理を足したいときに使います。ログ出力、実行時間の計測、キャッシュ、認証チェック、入力バリデーション、リトライ処理などが定番です。フレームワークでも多用されていて、Flaskの @app.route(...) や pytestの @pytest.fixture、組み込みの @property や @staticmethod もすべてデコレータです。
自作のデコレータは作れますか?
もちろん作れます。デコレータは「関数を受け取って関数を返す関数」なので、内側に小さな関数を定義して、その中で元の関数を呼び出す前後に処理を挟む、というのが基本パターンです。なお、内側の関数には functools.wraps を付けておくと、元の関数名やdocstringがそのまま保持されるのでおすすめです。