関数デコレータ

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

PythonにはJavaのアノテーションのように関数宣言の前に@から始まる文を書きますが、これを関数デコレータまたは単にデコレータと呼びます。

アノテーションとはまったく別物なので注意してください。(アノテーションの説明はこちら

関数デコレータとは

さて、関数デコレータとは何でしょうか。

簡単に説明すると、関数に対して機能を追加・変更するための機能です。

また、元の関数の処理には手が入らない点が大きなメリットです。

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

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

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

Pythonの関数はオブジェクトとして取り扱いが可能である、という説明を以前しました。

また、引数や戻り値に関数オブジェクトを含むようなものを高階関数と呼びました。

ピンとこない方は以下を復習してください。

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

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

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

    return new_func

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

    return ret

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

実行結果

start
55
end

解説します。

まず、deco_funcという関数は、内部でnew_funcという内部関数を定義しています。

このnew_funcは、deco_funcの関数を内部で実行していますが、その処理に加え、startという文字列、処理結果、endという文字列の3つを出力しています。

deco_funcはこの内部関数を新たな関数オブジェクトとして返却しているわけです。

新たな関数new_funcを実行すると、処理前後にstart、endが出力され、処理結果が出力されます。

関数デコレータ

上の高階関数では、元の関数に手を加えずに処理の追加ができている点に注意してください。

さて、この高階関数を常に適用したい場合、どうすればよいでしょうか?

そんな時に、デコレータを利用します。

上の関数をデコレータを利用したものに書き換えてみましょう。

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

    return new_func

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

    return ret


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

柔軟な引数

さて、上のサンプルでは引数なしの関数だけしか使えません。

通して読まれている方はすぐにピンときたと思いますが、そう、可変長引数を利用するとこの問題が解決できます。

さらに書きなおしてみましょう。

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

    return new_func

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

    return ret

my_func(1, 10)

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