関数

【Python】デコレータの基本的な使い方

【Python】デコレータの基本的な使い方

Pythonのデコレータの基本的な使い方について解説します。

デコレータ(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_valuefuncに渡され、wrapperにセットされた状態でmeasure_funcに返されます。measure_funcは、wrapper関数であり、その内部でfunc(*args, **kwargs)として元のsum_range_valueが実行されます。

このように、高階関数によって関数の機能拡張を行うことができます。

Note

高階関数とクロージャは、関数型言語(例えば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などのメタ情報が内部関数のものになるので注意してください。

デコレータは、関数に対して汎用的な機能拡張を行う際に非常に便利な方法です。ぜひ使い方を覚えて活用してみてください。