Kodokon kodokon.com

デコレータと高階関数

関数が第一級のオブジェクトであることを活かして、堅牢で、引数を取れて、透明なデコレータを書きましょう。

10 分 · 3 問

このレッスンを Kodokon で開く

Pythonでは、関数もほかの値と同じひとつのオブジェクトです。変数に代入し、引数として渡し、返すことができます。高階関数とは、関数を受け取るか、関数を返す関数のことです。sorted(data, key=...)もそのひとつです。そこにクロージャ、つまり内側の関数が外側の関数の変数を捕らえ、外側の関数が終わったあともそれを生かし続けるしくみを加えれば、デコレータの材料はすべてそろいます。

PYTHON
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
クロージャ:multiplyがfactorを捕らえる

デコレータとは、関数を受け取って別の関数を返す関数です。返されるのはたいてい、呼び出しの周りに振る舞いを付け足すwrapperです。def slow_sumの上に置かれた@timedという記法は、slow_sum = timed(slow_sum)の厳密な糖衣構文にすぎません。ラッパーはシグネチャに縛られないよう*args, kwargsを受け取り、デコレートされた関数の結果を返します**。これを忘れるのは典型的なミスで、あらゆる戻り値がNoneになってしまいます。

PYTHON
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__メソッドを持つクラスのほうが読みやすいことがよくあります。デコレータは横断的な関心事のためにとっておきましょう。ログ、キャッシュ、リトライ、アクセス制御などです。業務ロジックを書き換えてしまうデコレータは、肝心なところを読み手から隠してしまいます。

PYTHON
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")
引数を取るデコレータ:ファクトリ、デコレータ、ラッパー

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. def slow_sumの上に置かれた@timedという記法は、何と等価ですか?
    • timed(slow_sum())
    • slow_sum = timed(slow_sum)
    • slow_sum.timed = True
  2. @functools.wrapsはどんな問題を解決しますか?
    • デコレートされた関数の呼び出しを速くする
    • デコレートされた関数の名前とdocstringを保つ
    • デコレータをクラスのメソッドと互換にする
  3. @repeat(times=3)において、repeat(times=3)という呼び出しは何を返しますか?
    • デコレートされた関数の結果
    • デコレータ。関数を受け取る準備ができている
    • 最終的なラッパーを直接
    • 三つの結果のリスト