関数が第一級のオブジェクトであることを活かして、堅牢で、引数を取れて、透明なデコレータを書きましょう。
このレッスンを Kodokon で開くPythonでは、関数もほかの値と同じひとつのオブジェクトです。変数に代入し、引数として渡し、返すことができます。高階関数とは、関数を受け取るか、関数を返す関数のことです。sorted(data, key=...)もそのひとつです。そこにクロージャ、つまり内側の関数が外側の関数の変数を捕らえ、外側の関数が終わったあともそれを生かし続けるしくみを加えれば、デコレータの材料はすべてそろいます。
def make_multiplier(factor):
def multiply(value):
return value * factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10), triple(10)) # 20 30デコレータとは、関数を受け取って別の関数を返す関数です。返されるのはたいてい、呼び出しの周りに振る舞いを付け足すwrapperです。def slow_sumの上に置かれた@timedという記法は、slow_sum = timed(slow_sum)の厳密な糖衣構文にすぎません。ラッパーはシグネチャに縛られないよう*args, kwargsを受け取り、デコレートされた関数の結果を返します**。これを忘れるのは典型的なミスで、あらゆる戻り値がNoneになってしまいます。
import functools
import time
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
delay = time.perf_counter() - start
print(f"{func.__name__}: {delay:.4f}s")
return result
return wrapper
@timed
def slow_sum(n):
return sum(range(n))
print(slow_sum(1_000_000))デコレータに引数を持たせるには(@repeat(times=3))、階層をもうひとつ増やす必要があります。引数を受け取ってデコレータを返すファクトリがあり、そのデコレータがラッパーを返します。入れ子になった三つの関数。それが柔軟さの代償です。状態が複雑になるなら、__call__メソッドを持つクラスのほうが読みやすいことがよくあります。デコレータは横断的な関心事のためにとっておきましょう。ログ、キャッシュ、リトライ、アクセス制御などです。業務ロジックを書き換えてしまうデコレータは、肝心なところを読み手から隠してしまいます。
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def greet(name):
print(f"Hello {name}")
greet("Ada")