関数

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

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

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

デコレータ(decorator)

デコレータ(decorator)とは、関数に機能を追加するための仕組みのことを言います。

関数は基本的には一つのことを機能にまとめるのが再利用性、保守性の面から望ましいです。しかし、プログラミングをしていると例えばですが以下のように思うことがあります。

  • 関数実行時に関数の実行時間を計測したい
  • 関数実行時にログを出力をしたい
  • 関数の情報を表示したい  等々

上記はあくまで一例ですが、このような機能は特定の関数に依存しないような汎用的な機能で、毎回個別の機能に処理を書いていたら大変です。このような時にデコレータを使うと、関数に機能を簡単に付加することができます。

decorateは「飾る・装飾する」という意味があることから、デコレータは関数を飾って機能を付加する仕組みということだと思ってください。

本記事では、Pythonでデコレータ(decorator)を使用する基本について説明します。また、説明にあたって理解が必要な高階関数クロージャといったトピックスについても事前に説明します。

デコレータを使わない場合(高階関数での機能拡張)

デコレータは、別の表現をすると「高階関数での機能の拡張をシンプルに表現できる記法」ということもできます。

そのため、まずは、高階関数での機能拡張の方法とクロージャについて説明します。とにかくデコレータの使い方を早く知りたいという方は「デコレータの使い方」から読んでいただいて構いません。

高階関数とクロージャを文章で書いてみると以下のようになります。

  • 高階関数:関数自体を引数/戻り値として扱う関数
  • クロージャ:上位のローカル変数を保持した関数

文字だけで見るとよく分からないと思うので、例を見ながら説明します。以下の例は、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()と記載すると関数を実行するという意味ですが、関数名の「wrapper」のみで()がない場合は関数自体を意味します。

また、wapperという関数は、measure_execution_timeのfuncという変数を保持したまま返却されます。このように、上位の関数の引数の値を保持したままの関数クロージャと言います。つまり、wrapperを実行するとfuncを前提として処理が動くわけです。

【呼び出し側の処理について】

関数呼び出し時の処理の流れを順を追って解説していきます。

measure_func = measure_execution_time(sum_range_value)

ここでは、sum_range_valueという関数自体を渡しています。このsum_range_valueは、measure_execution_time関数の仮引数funcに渡され、wrapperにセットされた上でmeasure_func変数に返却されます。

sum_value = measure_func(1, 1000000)

measure_funcは、sum_range_valueという関数がセットされたwrapper関数なので、上記の呼び出しでは「def wrapper(*args, **kwargs):」の中の処理が実行がされます。

measure_funcに返却されてきたwrapperはクロージャで先ほどfuncにセットされたsum_range_valueを覚えているため「result = func(*args, **kwargs)」は、「result = sum_range_value(*args, **kwargs)」として実行されます。

    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")

wrapper関数の部分のfunc(この例ではsum_range_valueが設定されている)の実行前後で、time関数で関数実行前の時間、関数実行後の時間を取得し、その差分の時間を実行時間として出力しています。このようにすることで、sum_range_valueという関数に実行時間を計測して表示するという機能を追加することができています。

    measure_func = measure_execution_time(sample_func)
    result = measure_func()

もちろん別の関数として、例えばsample_funcが定義されているとして、同様に上記のような処理を実行すれば、今度はsample_funcが実行されたときの処理時間を計測して表示します。

以上が高階関数による機能拡張の例です。ただし、関数を引数に渡して戻り値の関数に対して実行するというのは、手順的にも分かりやすさの面でも少し不便です。

デコレータという仕組みは、この高階関数による機能拡張をシンプルに表現できる記法です。以降でデコレータの使い方について説明していきます。

Note

高階関数とクロージャについては、関数型言語(例えばHaskell等)を勉強していると遭遇する関数型プログラミングに関係のある領域です。興味がある方は関数型言語、関数型プログラミングについて調べてみると面白いかと思います。

デコレータの使い方

上記では高階関数でどのように関数の機能拡張をするのかについて説明しました。今度は全く同じ例をデコレータを使って表現してみます。デコレータを使って表現したプログラムは以下のようになります。

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
def sum_range_value(start, end, step=1):

sum_range_value定義の上にある「@measure_execution_time」のように「@関数名」というのがデコレータの表記になります。

これは「sum_range_value関数を、measure_execution_timeでデコレートする」という意味になります。

sum_value = sum_range_value(1, 1000000)

上記は関数の呼び出しの部分です。先ほどの高階関数を直接使用する例では「対象関数を引数に渡して、戻り値の関数を実行する」という面倒なことをしていましたが、単純にsum_range_valueという関数を呼び出すだけで、実行結果では計測時間が表示されていることが分かります。

このように、デコレータ(@関数名)という記載を使うだけで、簡単に関数の機能拡張を表現することができます。

デコレータ使用時の注意事項(メタ情報の保持)

デコレータでよくやってしまいがちな失敗が、オリジナル関数のメタ情報(docstring等)を保存し忘れてしまうことです。

上記のサンプルプログラムでは、メタ情報の保存に対応しているものです。あえて、docstringを表示してみています。docstringは、sum_range_value関数のdocstringであることはわかるかと思います。

このようにできているのは、functoolsのwrapsデコレーターを使用しているためです。デコレータ関数のwrapper関数宣言部分で以下のようにしている部分が該当します。

@wraps(func)
def wrapper(*args, **kwargs):

では、@wrapsデコレータを付けないとどうなるかを試してみて欲しいのですが、docstringは以下のようにwrapperのdocstringが表示されてしまい、sum_range_valueのdocstringが分からなくなってしまいます。

【実行結果】
内部ラッパー関数

私も最初デコレータを学んだ時にはここが分かっておらず、docstringがいまいち…と思っていました。ぜひ「functoolsのwrapsデコレータを使う」ということは覚えておいてもらえるとよいかと思います。

私は「エキスパートPythonプログラミング」という書籍を読んでこの方法について学びました。この書籍はPython応用編の書籍として色々な知見が得られますので、ぜひ読んでみていただけるとよいかと思います。

複数のデコレータを重ねて使用する

デコレータは、複数のデコレータを重ねて使うことが可能です。

以下では「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_function_info
def sum_range_value(start, end, step=1):

上記では、関数の適用順でいうとmeasure_execution_time(show_func_info(sum_range_value))というような関係性となっています。

数学の合成関数(例えば、f(g(x)))を思い出すかもしれませんが、まさにデコレータが実行しているのは合成関数に似ています。デコレータが関数型言語、関数型プログラミングに関係している理由です。

複数のデコレータを適用したい場合には、上記のようにデコレータ関数を@で続けて記載するだけで非常に簡単ですので、是非うまく使ってみてください。

まとめ

Pythonのデコレータ(decorator)の基本的な使い方について解説しました。また、デコレータの理解に必要となる高階関数クロージャといった考え方についても紹介しています。

デコレータを開発する場合は、functoolsのwrapsデコレータを使わないとdocstringが内部関数のものになってしまいます。デコレータを作るときは「functoolsのwrapsデコレータを使う」ということを覚えておいてもらうとよいかと思います。

デコレータは関数型言語、関数型プログラミングという領域にも関連があるような領域で、関数型プログラミングについては私も勉強中です。関数型プログラミングもなかなか奥深いものだと思いますので興味がある方は調べてみていただけると面白いかと思います。

デコレータは汎用的な関数の拡張を作成するのに非常に便利です。ぜひ使い方を覚えて活用してみてください。