デコレータ

デコレータで関数を拡張する

デコレータは、関数の処理を、その関数を変更することなく拡張できるしくみです。例えばトレースログ表示など、一時的に関数の処理以外に何かを追加したいときに活用できます。
次の例は、デコレータの考え方を、デコレータ構文を使わないで実装しています。func()は、引数を単にprint()で表示する関数として定義していますが、最後でfunc()を呼び出した結果は、引数に与えられた「10 ABC」の表示に加えて「!!!」が表示されています。
def dec(f):
    def wrap(a, b):
        f(a, b) 
        print('!!!')
    return wrap

def func(a, b):
    print(a, b)

func = dec(func)
func(10, "ABC")
10 ABC
!!!
func()関数を呼び出す前にdec()に関数自体を引数で与え、戻り値をfunc自身に上書き代入しています。
func = dec(func)
dec()関数は、引数fに関数を受け取ります。
dec()中で、関数wrap()をfunc()と同じ引数で定義しています。wrap()は、dec()の引数fに与えられた関数f(a, b)を呼び出した後で、「!!!」をprint()で表示する実装になっています。そして、dec()はwrap()を戻り値に返します。
dec()から返った戻り値で上書したfunc()は、warp()に置き換わっています。warp()は、元のfunc()を呼び出した後に「!!!」を表示するという実装なので、func()「=wrap()」を呼び出すと上のような結果が得られます。
この例のdec()は、引数で受け取る関数fに処理を装飾(デコレート)するデコレータ関数となります。しかしこの方法では、デコレートしたい関数の呼び出し前に「func = dec(func)」のような文を加える必要があります。デコレータ構文は、それを自動化できます。デコレートしたい関数の呼び出し前ではなく、その関数の定義の前に「@デコレータ関数」のように宣言するだけで、暗黙にデコレータ関数で関数を置き換える処理が行われます。
@デコレータ
def 関数():
    ...
上の例を、デコレータ構文で次のように書き換えることができます。
def dec(f):
    def wrap(a, b):
        f(a, b) 
        print('!!!')
    return wrap

@dec
def func(a, b):
    print(a, b)

func(10, "ABC")
10 ABC
!!!

引数を受け取るデコレータ 

デコレータに引数を与え、デコレータ関数がその引数を利用して関数を拡張できます。
@デコレータ(引数)
def 関数():
    ...
この場合のデコレータ関数の定義は、次のようになります。
def dec(引数):
    def dec1(関数):
        def wrap(...):
            関数(...)
            引数を使った追加の処理
        return wrap
    return dec1
dec()はdec1()関数を戻り値に返します。dec1()は、前節と同様な関数を引数に受け取るデコレータ関数であり、オリジナルの関数をデコレートするwrap()関数を戻り値に返します。wrap()の中では、最初のdec()の引数、すなわちデコレータ「@dec(引数)」の引数を利用できます。
次の例は、デコレータ構文を使わずに、引数を渡すデコレータを実装しています。
def dec(arg):
    def decwap(f):
        def wrap(a, b):
            f(a, b)
            print('!!! arg=', arg)
        return wrap
    return decwap

def func(a, b):
    print(a, b)

f = dec("XXX")      # decwrapが返る
func = f(func)      # wrapが返る
func(10, "ABC")
10 ABC
!!! arg= XXX
デコレータ関数にを渡すことと、デコレートしたい関数を渡すことの二段階の手続きを行います。
そしてこれは、引数付きデコレータで次のように書き換えられます。
def dec(arg):
    def decwap(f):
        def wrap(a, b):
            f(a, b)
            print('!!! arg=', arg)
        return wrap
    return decwap

@dec("XXX")
def func(a, b):
    print(a, b)

func(10, "ABC")
10 ABC
!!! arg= XXX

複数のデコレータを指定 

デコレータは複数指定できます。関数定義の前にデコレータ構文を重ねるように宣言します。この場合、先のデコレータが、その次のデコレータを取り込むように重なった関数になります。
@デコレータ1
@デコレータ2
def 関数():
    ...
これはデコレータを使わない場合で考えると、呼び出し時は次のようになります。
f = デコレータ2(関数)
関数 = デコレータ1(f)
関数()
次の例は、関数に複数のデコレータを宣言しています。
def deca(f):
    def wrap(a, b):
        f(a, b)     # この場合はdecbのwrap(a, b)
        print('deca wrap')
    return wrap

def decb(f):
    def wrap(a, b):
        f(a, b)
        print('decb wrap')
    return wrap

@deca
@decb
def func(a, b):
    print(a, b)

func(10, "ABC")
10 ABC
decb wrap
deca wrap
この結果から、先に指定したdecaの内側でdecbが実行されていることがわかります。
応用的に、引数付きので複数のデコレータを使う場合は次のようにできます。
def deca(arg):
    def decawap(f):
        def wrap(a, b):
            f(a, b)
            print('deca wrap')
            print('arg=', arg)
        return wrap
    return decawap

def decb(f):
    def wrap(a, b):
        f(a, b)
        print('decb wrap')
    return wrap

@deca('ZZZ')        # f = deca('ZZZ')
@decb               # func = f(decb(func))
def func(a, b):
    print(a, b)

func(10, "ABC")
10 ABC
decb wrap
deca wrap
arg= ZZZ

汎用的なデコレータ

これまでのデコレータの例は、デコレータのdec()が引数が(a, b)となっている関数を受け取ることを想定したものであり、それ以外の関数には適用できません。
デコレータ関数が返すラップ関数の引数定義を次のようにすれば、位置引数とキーワード引数のいずれの引数も受け入れることができるようになり、あらゆる関数に適用できるデコレータになります。
def wrap(*args, **kwargs):
次の例のデコレータdecは、引数の数が異なり、呼び出し時の引数の指定方法が異なっていても適応できています。
def dec(f):
    def wrap(*args, **kwargs):
        r =f(*args, **kwargs)   # あらゆる引数に対応
        print('dec wrap')
        return r
    return wrap

@dec
def func1(a, b):
    print("func1", a, b)
    return 100

@dec
def func2(a, b, c):
    print("func2", a, b, c)
    return 200

print(func1(10, "ABC")) 
print(func2("DEF", c=30, b=3.14)) 
func1 10 ABC
dec wrap
100
func2 DEF 3.14 30
dec wrap
200