Python のデコレータのスタック順序と実行順序

複数のデコレータを重ねた場合、適用順序と実行順序が異なります。この仕組みを理解しないと、予期しない動作が起きます。

デコレータの適用順序

デコレータは下から上に適用されます。

@decorator_a
@decorator_b
@decorator_c
def func():
    pass

# 等価な記述
func = decorator_a(decorator_b(decorator_c(func)))

最も内側(func に近い)の decorator_c が最初に適用されます。

実行順序

ラッパー関数の実行は、適用の逆順(上から下)になります。

def decorator(name):
    def wrapper(func):
        def inner(*args, **kwargs):
            print(f"{name}: before")
            result = func(*args, **kwargs)
            print(f"{name}: after")
            return result
        return inner
    return wrapper

@decorator("A")
@decorator("B")
@decorator("C")
def greet():
    print("Hello!")

greet()

出力:

A: before
B: before
C: before
Hello!
C: after
B: after
A: after

外側のデコレータから実行が始まり、内側に向かい、戻るときは逆順です。

順序が重要な例

from functools import wraps

def require_auth(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Checking authentication")
        return func(*args, **kwargs)
    return wrapper

def log_call(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

# ログを先に出したい場合
@log_call
@require_auth
def action():
    print("Action executed")

action()
# Calling wrapper ← log_call が wrapper を見ている
# Checking authentication
# Action executed

この例では log_call が require_auth のラッパーを見てしまいます。

@wraps の効果

wraps を使えば、関数名が保持されます。

# wraps を使った場合の出力
# Calling action
# Checking authentication
# Action executed

デコレータの順序を決める際は、実行順序を意識して設計しましょう。