Pythonインタラクティブガイド - ステップ3 関数 (9) - デコレータ
- 本講座「Pythonインタラクティブガイド」は、手を動かしながらPythonプログラミングの基礎を学べる実践的な講座です。
- 「スタイルガイド」では、Pythonで読みやすくきれいなコードを書くためのガイドライン(PEP8)を主に紹介しています。
- 各コード例はその場で実行して結果を確認できます。
ページ再読み込みで元に戻るので、自由に試してみてください。
「ステップ3 関数」の続きです。
3.8. デコレータ
前回は、関数を入力または出力として扱う「高階関数」について学びました。
今回は、高階関数の応用である「デコレータ」について学んでいきます。
3.8.1. デコレータとは
デコレータ(decorator)とは、既存の関数の振る舞いを変更するための仕組みです。
名前の通り、関数を「装飾」するものと考えることができます。
デコレータを使うと、次のようなことができます:
- 関数の実行前後に処理を追加する
- 関数の引数や戻り値を検証・変換する
- 関数の振る舞いを変更する(メモ化の機能追加など)
デコレータの仕組み
デコレータは本質的に「関数を引数として受け取り、新しい関数を返す関数」です。
関数をデコレータに入力すると、装飾された関数を返します。
そのため、デコレータも前回扱った高階関数の一種です。
まずは、シンプルなデコレータを見てみましょう:
このコードでは、次のような処理が行われています:
- デコレータ
my_decorator
は、関数func
を引数として受け取り、新しい関数wrapper
を返します wrapper
関数は、元の関数func
の実行前後に追加の処理を挟みますsay_hello
関数にデコレータを適用すると、装飾された新しい関数が得られます- その関数を呼び出すと、元の関数の処理に加えて、前後に追加処理も実行されます
デコレータによって返される関数は、内部で元の関数を呼び出しつつ、前後に追加処理を組み込みます。
このように元の関数を内包して「包み込む(ラップする)」役割を持つため、一般に wrapper
(ラッパー)関数と呼ばれます。
Pythonでは、このようにデコレータを使うことで、既存の関数に新しい処理を柔軟に追加できます。
関数の実行前に関数名を表示するデコレータ name_printer
を作成しましょう。
ただし、関数func
に対して、その名前はfunc.__name__
で取得できます。
解答例
3.8.2. デコレータ構文
Pythonでは、関数にデコレータを適用する簡単な構文が用意されています。
- 装飾したい関数の定義の上部に
@<デコレータ名>
を追加します。
先程の例は、以下のように書き換えることができます:
関数 say_hello
に @my_decorator
を付けることは、say_hello = my_decorator(say_hello)
と等価です。
このように、デコレータ構文を使うと、関数の装飾をより簡潔に記述できます。
前回のname_printer
デコレータを使ったプログラムを、デコレータ構文を使用して書き換えましょう。
解答例
複数のデコレータの適用
関数に複数のデコレータを適用することもできます:
複数のデコレータは下から上へ適用されることに注意してください。 つまり、コード中で最も関数定義に近いデコレータが最初に適用され、 その次に上のデコレータが適用されます。
外側のデコレータ(最後に適用)
内側のデコレータ(最初に適用)
print("こんにちは!")
3.8.3. 汎用的なデコレータ
これまでの例では、パラメータや戻り値を持たないシンプルな関数にだけデコレータを適用していました。
以下のような実装を行うと、任意の関数に対応した汎用的なデコレータを作成できます:
このコードの処理の流れは次のとおりです:
- デコレータ
my_decorator
は、関数func
を受け取り、新しい関数wrapper
を返します wrapper
関数は次の役割を持ちます:- 任意の関数の引数を受け取れるように、可変長引数(
args
,kwargs
パラメータ)を使用する - 元の関数
func
の実行前後に追加の処理を挟む - 受け取った引数をそのまま
func
に渡して実行し、結果をresult
に格納する result
を戻り値として返す
- 任意の関数の引数を受け取れるように、可変長引数(
- 関数にデコレータを適用すると、装飾された新しい関数が得られます
- その関数を呼び出すと、元の関数の処理に加えて前後に追加処理も実行され、元の関数と同じ戻り値が返ります
このように、可変長引数を使用することで、引数の異なる任意の関数に適用可能なデコレータが作成できます。
任意の関数の実行時間(秒)を計測するデコレータ measure_time
を作成しましょう。
ただし、実行時間の取得は以下のコードを参考にしてください:
解答例
3.8.4. デコレータを返す関数
デコレータも関数であり、オブジェクトとして扱えるため、デコレータを返す関数も作成できます:
このコードでは、次のことが起きています:
repeat(n=3)
を実行すると、decorator
関数が返されます- 返された
decorator
関数がsay_hello
関数に適用されます - 結果として、
say_hello
関数を呼び出すと、内部で元の関数が3回実行されます
前回の measure_time
デコレータを発展させ、trials
オプションで実行回数を指定すると、「関数をtrials
回実行して平均実行時間を表示するデコレータ」を返す関数 measure_avg_time
を作成しましょう。
解答例
3.8.5. functools.wraps
の活用
デコレータを使用する際の問題の1つは、デコレートされた関数が元の関数のメタデータ(名前、docstring、パラメータ情報など)を失ってしまうことです。これはデバッグやドキュメント生成において問題になります。
この問題は functools.wraps
関数を使用することで解決できます:
functools.wraps
は、前小節で扱った「デコレータを返す関数」の一種です。
関数を受け取ると、その関数のメタデータ(名前、docstring、パラメータ情報など)を、装飾対象の関数へ引き継ぐデコレータを返します。
これをラッパー関数に適用することで、デコレータを使用しても元の関数のメタデータが保持され、
デバッグやドキュメント生成が容易になります。
前の練習問題で扱ったmeasure_time
デコレータで functools.wraps
を使用して、関数の情報が引き継がれるようにしましょう。
解答例
まとめ
この節では、Pythonのデコレータについて以下のポイントを学びました:
- デコレータとは: 既存の関数の振る舞いを変更するための仕組み(高階関数の一種)
- デコレータ構文:
@デコレータ名
という簡潔な構文でデコレータを適用できる - 汎用的なデコレータ: 任意の関数に対応するデコレータを作成する方法
- デコレータを返す関数: パラメータを受け取ってデコレータを作成する方法
- functools.wrapsの活用: デコレータ使用時に元の関数のメタデータを保持する方法
デコレータはPythonの強力な機能の一つで、ログ記録、実行時間計測、入力検証など、さまざまな処理を簡単に追加できます。 こうした追加の処理を本来の処理から独立して実装することで、コードの見通しがよくなり再利用性も高まります。
次回は、大量のデータを効率的に処理する際に重要な「ジェネレータ関数」について学びます。