コンテンツ

Pythonインタラクティブガイド - ステップ3 関数 (9) - デコレータ

シリーズ - Pythonインタラクティブガイド
Info
  • 本講座「Pythonインタラクティブガイド」は、手を動かしながらPythonプログラミングの基礎を学べる実践的な講座です。
  • 「スタイルガイド」では、Pythonで読みやすくきれいなコードを書くためのガイドライン(PEP8)を主に紹介しています。
  • 各コード例はその場で実行して結果を確認できます。
    ページ再読み込みで元に戻るので、自由に試してみてください。

「ステップ3 関数」の続きです。

前回は、関数を入力または出力として扱う「高階関数」について学びました。
今回は、高階関数の応用である「デコレータ」について学んでいきます。

デコレータ(decorator)とは、既存の関数の振る舞いを変更するための仕組みです。
名前の通り、関数を「装飾」するものと考えることができます。

デコレータの役割

デコレータを使うと、次のようなことができます:

  • 関数の実行前後に処理を追加する
  • 関数の引数や戻り値を検証・変換する
  • 関数の振る舞いを変更する(メモ化の機能追加など)

デコレータは本質的に「関数を引数として受け取り、新しい関数を返す関数」です。
関数をデコレータに入力すると、装飾された関数を返します。
そのため、デコレータも前回扱った高階関数の一種です。

まずは、シンプルなデコレータを見てみましょう:

このコードでは、次のような処理が行われています:

  1. デコレータ my_decorator は、関数 func を引数として受け取り、新しい関数 wrapper を返します
  2. wrapper 関数は、元の関数 func の実行前後に追加の処理を挟みます
  3. say_hello 関数にデコレータを適用すると、装飾された新しい関数が得られます
  4. その関数を呼び出すと、元の関数の処理に加えて、前後に追加処理も実行されます

デコレータによって返される関数は、内部で元の関数を呼び出しつつ、前後に追加処理を組み込みます。
このように元の関数を内包して「包み込む(ラップする)」役割を持つため、一般に wrapper(ラッパー)関数と呼ばれます。

Pythonでは、このようにデコレータを使うことで、既存の関数に新しい処理を柔軟に追加できます。

📚練習問題

関数の実行前に関数名を表示するデコレータ name_printer を作成しましょう。
ただし、関数funcに対して、その名前はfunc.__name__で取得できます。

解答例

Pythonでは、関数にデコレータを適用する簡単な構文が用意されています。

デコレータ構文
@デコレータ名 def 関数名(): ...
  • 装飾したい関数の定義の上部に@<デコレータ名>を追加します。

先程の例は、以下のように書き換えることができます:

関数 say_hello@my_decorator を付けることは、say_hello = my_decorator(say_hello) と等価です。
このように、デコレータ構文を使うと、関数の装飾をより簡潔に記述できます。

📚練習問題

前回のname_printer デコレータを使ったプログラムを、デコレータ構文を使用して書き換えましょう。

解答例

関数に複数のデコレータを適用することもできます:

複数のデコレータは下から上へ適用されることに注意してください。 つまり、コード中で最も関数定義に近いデコレータが最初に適用され、 その次に上のデコレータが適用されます。

@decorator1
外側のデコレータ(最後に適用)
@decorator2
内側のデコレータ(最初に適用)
say_hello 本体
print("こんにちは!")

これまでの例では、パラメータや戻り値を持たないシンプルな関数にだけデコレータを適用していました。
以下のような実装を行うと、任意の関数に対応した汎用的なデコレータを作成できます:

このコードの処理の流れは次のとおりです:

  1. デコレータ my_decorator は、関数 func を受け取り、新しい関数 wrapper を返します
  2. wrapper関数は次の役割を持ちます:
    1. 任意の関数の引数を受け取れるように、可変長引数(args, kwargsパラメータ)を使用する
    2. 元の関数 func の実行前後に追加の処理を挟む
    3. 受け取った引数をそのまま func に渡して実行し、結果を result に格納する
    4. result を戻り値として返す
  3. 関数にデコレータを適用すると、装飾された新しい関数が得られます
  4. その関数を呼び出すと、元の関数の処理に加えて前後に追加処理も実行され、元の関数と同じ戻り値が返ります

このように、可変長引数を使用することで、引数の異なる任意の関数に適用可能なデコレータが作成できます。

📚練習問題

任意の関数の実行時間(秒)を計測するデコレータ measure_time を作成しましょう。

ただし、実行時間の取得は以下のコードを参考にしてください:

解答例

デコレータも関数であり、オブジェクトとして扱えるため、デコレータを返す関数も作成できます:

このコードでは、次のことが起きています:

  1. repeat(n=3) を実行すると、decorator 関数が返されます
  2. 返された decorator 関数が say_hello 関数に適用されます
  3. 結果として、say_hello 関数を呼び出すと、内部で元の関数が3回実行されます
📚練習問題

前回の measure_time デコレータを発展させ、trials オプションで実行回数を指定すると、「関数をtrials回実行して平均実行時間を表示するデコレータ」を返す関数 measure_avg_time を作成しましょう。

解答例

デコレータを使用する際の問題の1つは、デコレートされた関数が元の関数のメタデータ(名前、docstring、パラメータ情報など)を失ってしまうことです。これはデバッグやドキュメント生成において問題になります。

この問題は functools.wraps 関数を使用することで解決できます:

functools.wraps は、前小節で扱った「デコレータを返す関数」の一種です。
関数を受け取ると、その関数のメタデータ(名前、docstring、パラメータ情報など)を、装飾対象の関数へ引き継ぐデコレータを返します。

これをラッパー関数に適用することで、デコレータを使用しても元の関数のメタデータが保持され、
デバッグやドキュメント生成が容易になります。

📚練習問題

前の練習問題で扱ったmeasure_timeデコレータで functools.wraps を使用して、関数の情報が引き継がれるようにしましょう。

解答例

この節では、Pythonのデコレータについて以下のポイントを学びました:

  1. デコレータとは: 既存の関数の振る舞いを変更するための仕組み(高階関数の一種)
  2. デコレータ構文: @デコレータ名 という簡潔な構文でデコレータを適用できる
  3. 汎用的なデコレータ: 任意の関数に対応するデコレータを作成する方法
  4. デコレータを返す関数: パラメータを受け取ってデコレータを作成する方法
  5. functools.wrapsの活用: デコレータ使用時に元の関数のメタデータを保持する方法

デコレータはPythonの強力な機能の一つで、ログ記録、実行時間計測、入力検証など、さまざまな処理を簡単に追加できます。 こうした追加の処理を本来の処理から独立して実装することで、コードの見通しがよくなり再利用性も高まります。

次回は、大量のデータを効率的に処理する際に重要な「ジェネレータ関数」について学びます。

関連記事