デコレーターとは、渡された関数の振る舞いを拡張したり置き換えたりするために、別の関数を引数として受け取る関数です。
関数Aがデコレーターで、関数Bがその引数であるとき、関数Aは関数Bのコードを_変更することなく_、関数Bの振る舞いを変更、拡張、置き換えします。
デコレーター関数Aが関数Bを_ラップする_と言います。
「振る舞いを変更する」と言っていますが、ラップされた関数自体は_実際には変更されません_。
振る舞いは、ラップされた関数の_まわり_(と、それが返すもの)に追加されるか、その振る舞いが別の振る舞いに_置き換わる_かのどちらかです。
高階関数とは、1つ以上の関数を引数として受け取ったり、1つ以上の関数を返したりする関数です。 引数として使ったり、別の関数から返したりできる関数を、第一級関数と呼びます。 Pythonの関数は、呼び出し可能オブジェクトとして、他の値やオブジェクトと同じように、変数に代入したり引数として使ったりできる第一級関数です。 高階関数と第一級関数が組み合わさることで、デコレーターが実現できます。
@記号は、デコレーター関数の名前の前に付けて、デコレートしたい関数のすぐ上に書きます。
@decorator
def decorated_function():
pass
引数を受け取るデコレーターもあります。
@decorator_with_arg(name="Bob")
def decorated_function2():
pass
デコレーターにデフォルト引数が定義されている場合は、他の関数を呼び出すときと同じように、デコレーターを機能させるには@decorator()のように括弧(())を付けて呼び出さなければなりません。
@decorator_with_default_arg()
def decorated_function3():
pass
デコレーターが_位置引数_を取る場合、その引数を渡さないと、次のようなエラーになります。
TypeError: decorator_with_pos_arg() missing 1 required positional argument: 'name'
@decoratorという構文は、_デコレートする側の関数_を呼び出し、_デコレートされる関数_を引数として渡すための糖衣構文(シンタックスシュガー)、つまり省略記法です。
デコレーターを呼び出す別の方法の例を、次に示します。
def function():
pass
function = decorator(function)
def function2():
pass
function2 = decorator_with_arg(name="Bob")(function2)
def function3():
pass
function3 = decorator_with_default_arg()(function3)
ほとんどのデコレーターは、別の関数の振る舞いを_拡張_したり_置き換え_たりするためのものですが、ラップしている関数をそのまま返すだけのデコレーターもあります。
デコレーターは、少なくとも1つの引数、つまりラップする関数を受け取る関数です。 通常は、ラップした関数そのものか、その関数を使った式の結果を返します。
シンプルなデコレーター、つまりラップした関数をそのまま返すだけのデコレーターは、次のように書けます。
>>> def do_nothing(func):
... return func
...
... @do_nothing
... def function4():
... return 4
...
>>> print(function4())
4
デコレーターは、ログに使う追加情報のような副作用だけを加えることもあります。
>>> def my_logger(func):
... print(f"Entering {func.__name__}")
... return func
...
... @my_logger
... def my_func():
... print("Hello")
...
>>> my_func()
Entering my_func
Hello
デコレーターは自分自身を返すわけではありません。 自分の関数引数を返すこともあれば、別の関数を返すこともあり、渡された関数(デコレートした関数)の戻り値を置き換える1つ以上の値を返すこともあります。 デコレーターが別の関数を返す場合、通常は内部関数を返します。
関数は、関数の中でも定義できます。 このように入れ子になった関数を、内部関数と呼びます。 デコレーターは、内部関数を使って自分の関数引数をラップできます。 デコレーターは、その内部関数を返します。 内部関数は、元の関数引数を返すこともあります。
次は、検証にデコレーターを使った例です。
def my_validator(func):
def my_wrapper(world):
print(f"Entering {func.__name__} with {world} argument")
if world == "Pluto":
print("Pluto is not a planet!")
else:
return func(world)
return my_wrapper
@my_validator
def my_func(planet):
print(f"Hello, {planet}!")
my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!
my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Pluto is not a planet!
最初の行では、デコレーターのfunc引数が定義されています。
次の行では、func引数をラップする、デコレーターの_内部関数_が定義されています。
_内部関数_はデコレーターのfunc引数をラップするので、funcに渡されるのと同じ引数を受け取ります。
なお、ラッパーは、funcで定義された引数と同じ名前を使う必要はありません。
元の関数ではplanet、デコレーターではworldという名前を使っていますが、デコレーターはちゃんと動きます。
内部関数は、funcを返すか、world == "Pluto"の場合にはPlutoは惑星ではないと出力します。
代わりにValueErrorを発生させるように書くこともできます。
つまり、_内部関数_はfuncをラップし、funcを返すか、funcがするはずのことの代わりになる何かを行います。
デコレーターは、自分の_内部関数_を返します。
_内部関数_が、渡された元の関数を返すこともあれば、返さないこともあります。
ラッパー関数または_内部関数_の中で条件によってどのコードが実行されるかによって、funcが返されたり、エラーが発生したり、funcの戻り値と同じ型の値が返されたりします。
*argsと**kwargsの構文を使えば、任意の数の引数を取る関数のためのデコレーターも書けます。
次は、任意の数の引数を取る関数のためのデコレーターの例です。
def double(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs) * 2
return wrapper
@double
def add(*args):
return sum(args)
print(add(2, 3, 4))
#-> 18
print(add(2, 3, 4, 5, 6))
#-> 40
これは、関数引数の戻り値を2倍にする仕組みです。 戻り値を3倍、4倍などにしたい場合は、次のセクションで示すように、デコレーター自体に仮引数を追加します。
次は、デコレートした関数の戻り値を好きな倍数にできるよう設定できるデコレーターの例です。
def multi(factor=1):
if factor == 0:
raise ValueError("factor must not be 0")
def outer_wrapper(func):
def inner_wrapper(*args, **kwargs):
return func(*args, **kwargs) * factor
return inner_wrapper
return outer_wrapper
@multi(factor=3)
def add(*args):
return sum(args)
print(add(2, 3, 4))
#-> 27
print(add(2, 3, 4, 5, 6))
#-> 60
最初の行で、factorが0でないことを検証しています。
次に、外側のラッパーを定義します。
これは、仮引数を取らないデコレーターに期待されるのと同じシグネチャーです。
外側のラッパーには、元の関数と同じシグネチャーを持つ内部関数があります。
内側のラッパーが、元の関数の戻り値に、デコレーターに渡された引数を掛ける処理を行います。
外側のラッパーは内側のラッパーを返し、デコレーターは外側のラッパーを返します。
次は、元の関数に渡された引数を検証するかどうかを制御できる、仮引数を持つデコレーターの例です。
def check_for_pluto(check=True):
def my_validator(func):
def my_wrapper(world):
print(f"Entering {func.__name__} with {world} argument")
if check and world == "Pluto":
print("Pluto is not a planet!")
else:
return func(world)
return my_wrapper
return my_validator
@check_for_pluto(check=False)
def my_func(planet):
print(f"Hello, {planet}!")
my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!
my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Hello, Pluto!
これで、Plutoをチェックするかどうかを簡単に切り替えられ、しかもmy_funcを変更する必要はありません。