带参数的装饰器

需要再多套一层函数:

from functools import wraps

def repeat(times):                   # 接收装饰器参数
    def decorator(func):             # 接收函数
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def hello():
    print("hi")

hello()
# hi
# hi
# hi

记忆口诀:没参数的装饰器 = 一层;有参数的 = 两层

多层装饰器堆叠

@log
@timer
def slow_add(a, b):
    return a + b

# 等价于
slow_add = log(timer(slow_add))
# 调用顺序:log 在外、timer 在内

自下而上"包",自上而下"调"——记住这条规律。

类装饰器:用类当装饰器

class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"第 {self.count} 次调用")
        return self.func(*args, **kwargs)

@CountCalls
def hello():
    print("hi")

hello()    # 第 1 次调用 → hi
hello()    # 第 2 次调用 → hi
print(hello.count)    # 2

类装饰器适合需要状态的场景(计数、缓存、限流)。

装饰器装饰类

def add_repr(cls):
    def __repr__(self):
        attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
        return f"{cls.__name__}({attrs})"
    cls.__repr__ = __repr__
    return cls

@add_repr
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

print(Point(3, 4))     # Point(x=3, y=4)

dataclass 内部就是用类似机制做的。

实用装饰器:限流

import time
from functools import wraps

def throttle(seconds):
    def decorator(func):
        last_call = [0]
        @wraps(func)
        def wrapper(*args, **kwargs):
            now = time.time()
            if now - last_call[0] < seconds:
                print("⚠ 太频繁")
                return None
            last_call[0] = now
            return func(*args, **kwargs)
        return wrapper
    return decorator

@throttle(seconds=2)
def click():
    print("clicked")

click()
click()    # ⚠ 太频繁

实用装饰器:重试

import time
from functools import wraps

def retry(attempts=3, delay=1):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for i in range(attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"失败 {i+1}/{attempts}: {e}")
                    time.sleep(delay)
            raise
        return wrapper
    return decorator

@retry(attempts=3, delay=2)
def fetch():
    ...

装饰器调试技巧

装饰过的函数报错时 traceback 看起来很乱——加 @wraps 至少能保留函数名。要更深入查:"去装饰器跑一次原函数"看错是不是装饰器的锅。

下一篇讲迭代器协议——for 循环背后的机制。