デコレータ

まえがき(アノテーションではないので注意)

PythonにはJavaのアノテーションのように関数宣言の前に@から始まる文を書きますが、これを関数デコレータまたは単にデコレータと呼びます。アノテーションとはまったく別物なので注意してください。(アノテーションの説明はこちら

デコレータとは

デコレータとは高階関数を使用して既存の関数に対して機能を追加・変更するための機能です。元の関数の処理内部に手を加えずに機能を追加・変更できるという点が大きなメリットです。別ページで解説しますが、クラスにもデコレータを使用することができ、それらと区別する際は関数デコレータと呼びます。

ただし、いきなりデコレータの説明から入ると大抵理解できません。(実際、デコレータを苦手とする方は多いようです。)

このため、復習を兼ねて順に説明していきましょう。

関数オブジェクトと高階関数の復習

Pythonの関数はオブジェクトとして取り扱いが可能である、という説明を以前しました。また、引数や戻り値に関数オブジェクトを含むようなものを高階関数と呼びました。ピンとこない方は以下を復習してください。

関数オブジェクト
内部関数とnonlocal宣言
lambda式

まず、内部関数と組み合わせた高階関数のサンプルを以下に示します。

def deco_func(f):
    def new_func():
        print('start')
        val = f()
        print('end')
        return val

    return new_func


def my_func():
    """ 1から10までの合計を返す関数 """
    ret = 0
    for i in range(1, 11):
        ret += i
    print("my_func実行中")
    return ret


f = deco_func(my_func)  # 新たに機能を追加した関数オブジェクトを作成する
x = f()  # 新たに作成した関数を実行する
print(x)

実行結果

start
my_func実行中
end
55

deco_funcは引数で指定された関数に対して処理の前後にstart、endという文字列の3つを出力するように機能の改変を行ったものを関数オブジェクトとして返しています。
deco_func内をもう少し詳しくみてみましょう。内部でnew_funcという内部関数を定義しています。このnew_funcは、deco_funcが引数で受け取った関数を実行していますが、その処理に加え、startという文字列、処理結果、endという文字列の3つを出力しています。deco_funcはこの内部関数を新たな関数オブジェクトとして返却しているわけです。新たな関数new_funcを実行すると、処理前後にstart、endが出力され、処理結果が出力されます。

デコレータ

上の高階関数では、元の関数に手を加えずに処理の追加ができている点に注意してください。さて、この高階関数を常に適用したい場合、どうすればよいでしょうか?そこでいよいよデコレータの出番です。以下のように処理を追加したい関数の上に@をつけて高階関数を記述します。

デコレータ
@高階関数
def 関数名(引数):
    :
    :

デコレータがつけられた関数は常に@以降で指定した高階関数が適用された状態に変化します。さきほどの関数をデコレータを利用したものに書き換えてみましょう。

def deco_func(f):
    def new_func():
        print('start')
        val = f()
        print('end')
        return val

    return new_func


@deco_func
def my_func():
    """ 1から10までの合計を返す関数 """
    ret = 0
    for i in range(1, 11):
        ret += i
    print("my_func実行中")
    return ret


x = my_func() # 普通にmy_funcを実行すると、deco_funcが適用された関数が実行される。
print(x)

デコレータと可変長引数

上のサンプルでは引数なしの関数だけしか使えません。入門編を通して読まれている方はすぐにピンときたと思いますが、そう、可変長引数を利用するとこの問題が解決できます。さらに書きなおしてみましょう。

def deco_func(f):
    def new_func(*args, **kwargs):
        print('start')
        val = f(*args, **kwargs)
        print('end')
        return val

    return new_func


@deco_func
def my_func(n, m):
    """ nからmまでの合計を返す関数 """
    ret = 0
    for i in range(n, m + 1):
        ret += i

    print("my_func実行中")
    return ret


x = my_func(1, 10)
print(x)

いかがでしょうか?少し難しいですが、前述の通り元の関数に手を加えずに機能追加ができますので、積極的に活用したいですね。