Pythonのデコレータの基本的な使い方について解説します。
Contents
デコレータ(decorator)
デコレータとは、関数に機能を追加するための仕組みです。例えば、関数の実行時間を計測したり、ログを出力したりするなど、関数に依存しない汎用的な機能を簡単に付加できます。「飾る・装飾する」という意味から、デコレータは関数を装飾して機能を追加するものと理解してください。
この記事では、Pythonのデコレータの基本的な使い方について紹介します。また、理解を深めるために高階関数やクロージャについても説明します。
高階関数とクロージャ
デコレータは「高階関数での機能の拡張をシンプルに表現する記法」とも言えます。理解を助けるために、高階関数とクロージャについて説明します。
- 高階関数:関数を引数や戻り値として扱う関数
- クロージャ:上位のスコープの変数を保持する関数
以下の例では、高階関数を用いて、sum_range_value
関数に「実行時間を計測する機能」を追加しています。
import time def measure_execution_time(func): def wrapper(*args, **kwargs): # 処理前の時刻を設定 timer_start = time.time() # 対象関数の実行 result = func(*args, **kwargs) # 処理後の時刻を設定 timer_end = time.time() # 処理時間を計算 elapsed_time = timer_end - timer_start print(f"処理実行時間: {elapsed_time} sec") return result return wrapper def sum_range_value(start, end, step=1): result = 0 for i in range(start, end + 1, step): result += i return result def main(): measure_func = measure_execution_time(sum_range_value) sum_value = measure_func(1, 1000000) print(sum_value) if __name__ == "__main__": main()
【実行結果例】 処理実行時間: 0.044879913330078125 sec 500000500000
measure_execution_time
関数は、関数を引数として受け取り、wrapper
関数を返却します。このように、関数自体を引数や戻り値に持つ関数を高階関数と言います。
また、wrapper
関数は、measure_execution_time
の引数であるfunc
を保持したまま返されるため、これをクロージャと呼びます。
呼び出しの流れでは、measure_execution_time(sum_range_value)
によりsum_range_value
がfunc
に渡され、wrapper
にセットされた状態でmeasure_func
に返されます。measure_func
は、wrapper
関数であり、その内部でfunc(*args, **kwargs)
として元のsum_range_value
が実行されます。
このように、高階関数によって関数の機能拡張を行うことができます。
高階関数とクロージャは、関数型言語(例えばHaskell、Scalaなど)を学ぶときによく登場する概念です。興味があれば関数型プログラミングも学んでみると良いでしょう。
デコレータの使い方
高階関数による機能拡張は、デコレータを使うことで簡単に実現できます。
import time from functools import wraps def measure_execution_time(func): """時間計測デコレータ関数 Args: func: 対象関数 Returns: wrapper関数 """ @wraps(func) def wrapper(*args, **kwargs): """内部ラッパー関数""" # 処理前の時刻を設定 timer_start = time.time() # 対象関数の実行 result = func(*args, **kwargs) # 処理後の時刻を設定 timer_end = time.time() # 処理時間を計算 elapsed_time = timer_end - timer_start print(f"処理実行時間: {elapsed_time} sec") return result return wrapper @measure_execution_time def sum_range_value(start, end, step=1): """範囲内の数値を合計する関数 Args: start: 開始数値 end: 終了数値 step: ステップ値 Returns: 合計結果 """ result = 0 for i in range(start, end + 1, step): result += i return result def main(): sum_value = sum_range_value(1, 1000000) print(sum_value) print(f"\ndocstring:\n{sum_range_value.__doc__}") if __name__ == "__main__": main()
【実行結果例】 処理実行時間: 0.03454852104187012 sec 500000500000 docstring: 範囲内の数値を合計する関数 Args: start: 開始数値 end: 終了数値 step: ステップ値 Returns: 合計結果
デコレータは関数定義の上に「@デコレータ名
」と記述します。例えば@measure_execution_time
とすることで、sum_range_value
関数に実行時間の計測機能が付加されます。
このようにデコレータを使うことで関数の機能拡張が簡単に行えます。
デコレータ使用時の注意事項(メタ情報の保持)
デコレータを使用する際、オリジナル関数のメタ情報(docstringなど)が失われることがあります。この問題は、functools
の@wraps
デコレータを使うことで解決できます。
@wraps(func) def wrapper(*args, **kwargs):
@wraps
デコレータを使用しない場合、メタ情報が以下のようにwrapper
関数のものになってしまいますので注意が必要です。
【実行結果】 内部ラッパー関数
上記については「エキスパートPythonプログラミング」という書籍に記載があります。Python応用編として色々な知見が得られますので参考にしていただければと思います。
複数のデコレータを重ねて使用する
デコレータは、複数重ねて使用ができます。以下は、measure_execution_time
に加えて、関数名と引数情報を表示するshow_function_info
デコレータを追加した例です。
import time from functools import wraps def measure_execution_time(func): """時間計測デコレータ関数 Args: func: 対象関数 Returns: wrapper関数 """ @wraps(func) def wrapper(*args, **kwargs): """内部ラッパー関数""" # 処理前の時刻を設定 timer_start = time.time() # 対象関数の実行 result = func(*args, **kwargs) # 処理後の時刻を設定 timer_end = time.time() # 処理時間を計算 elapsed_time = timer_end - timer_start print(f"処理実行時間: {elapsed_time} sec") return result return wrapper def show_function_info(func): """関数情報表示用デコレータ関数 Args: func: 対象関数 Returns: wrapper関数 """ @wraps(func) def wrapper(*args, **kwargs): """内部ラッパー関数""" print("--------------------------------------------------------------") print(f"Name: {func.__name__}") print(f"args: {args}") print(f"kwargs: {kwargs}") print("--------------------------------------------------------------") return func(*args, **kwargs) return wrapper @measure_execution_time @show_function_info def sum_range_value(start, end, step=1): result = 0 for i in range(start, end + 1, step): result += i return result def main(): sum_val = sum_range_value(1, 1000000) print(sum_val) if __name__ == "__main__": main()
【実行結果例】 -------------------------------------------------------------- Name: sum_range_value args: (1, 1000000) kwargs: {} -------------------------------------------------------------- 処理実行時間: 0.044873714447021484 sec 500000500000
デコレータを複数適用する場合、上から順に適用され数学の合成関数のようにmeasure_execution_time(show_func_info(sum_range_value))
となります。
このように、複数のデコレータを重ねることで関数の拡張が簡単に表現できます。
まとめ
Pythonのデコレータの基本的な使い方について解説しました。また、デコレータの理解の助けとなる高階関数とクロージャに関する考え方も紹介しています。
デコレータは「@デコレータ名
」という表記を関数に付与するだけで簡単に関数に機能を付与することが可能です。ただし、functools
の@wraps
デコレータを使用しない場合、docstringなどのメタ情報が内部関数のものになるので注意してください。
デコレータは、関数に対して汎用的な機能拡張を行う際に非常に便利な方法です。ぜひ使い方を覚えて活用してみてください。
上記で紹介しているソースコードについてはgithubにて公開しています。参考にしていただければと思います。