デコレータ
デコレータで関数を拡張する
デコレータは、関数の処理を、その関数を変更することなく拡張できるしくみです。例えばトレースログ表示など、一時的に関数の処理以外に何かを追加したいときに活用できます。
次の例は、デコレータの考え方を、デコレータ構文を使わないで実装しています。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