带参数的装饰器,本质上是一个“装饰器工厂”。你写 @log(level="DEBUG") 的时候,Python 先调用 log(level="DEBUG"),拿到一个真正的装饰器,再拿这个装饰器去包装下面的函数。所以,三层嵌套是绕不开的:外层接收参数,中层接收函数,内层执行实际逻辑。漏掉任何一层,或者把 @wraps 放错位置,都会报错。下面咱们就把这个坑踩实了,再填上。
为什么直接套用 @decorator(func) 会报错?
因为带参数的装饰器本质是“装饰器工厂”,不是直接作用于函数的装饰器。你写 @log(level="DEBUG") 时,Python 实际调用的是 log(level="DEBUG"),它必须返回一个可调用对象(通常是闭包),这个对象才真正接收被装饰的函数。
常见错误是忘了多包一层函数,比如写成:
def log(level): print(f"Logging at {level}") return lambda func: func # ❌ 这里只打印一次,且没保存 level 到闭包中
结果是装饰器初始化时就执行了日志输出,且所有被装饰函数共享同一份 level 值(实际还没绑定)。
- 必须有三层嵌套:外层接收装饰器参数,中层接收函数,内层是实际 wrapper
- 中层函数(常叫
decorator)不能直接执行逻辑,它是被 Python 自动调用的入口 functools.wraps要用在最内层 wrapper 上,否则func.__name__会变成wrapper
如何正确写出三层结构并保留原函数元信息?
核心是严格遵循“参数 → 函数 → 调用”顺序,并用 functools.wraps 修复签名。下面是最小可靠模板:
from functools import wraps
def retry(max_attempts=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for i in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if i == max_attempts - 1:
raise
time.sleep(delay)
return None
return wrapper
return decorator
注意:@wraps(func) 必须放在 wrapper 定义之后、return wrapper 之前;decorator 是闭包,能访问外层 max_attempts 和 delay。
- 调用方式必须带括号:
@retry(max_attempts=2),即使参数全用默认值也不能省略() - 如果想支持无括号写法(如
@retry),需在decorator层判断入参类型——但容易出错,不建议初学尝试 args和kwargs要原样透传,否则会丢参数或类型错误
装饰器类写法比函数式更易维护吗?
当参数多、逻辑重(比如要记录调用次数、缓存状态、管理连接池),类写法更清晰。关键是实现 __call__ 并在 __init__ 存参:
class cached:
def __init__(self, timeout=300):
self.timeout = timeout
self.cache = {}
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
key = (args, tuple(sorted(kwargs.items())))
now = time.time()
if key in self.cache:
result, timestamp = self.cache[key]
if now - timestamp < self.timeout:
return result
result = func(*args, **kwargs)
self.cache[key] = (result, now)
return result
return wrapper
类的优势在于状态可封装(如 self.cache),但要注意:每个 @cached() 调用都会新建一个实例,self.cache 不跨装饰器共享。
- 类装饰器必须实现
__call__,且返回 wrapper,不能在__init__里直接返回函数 - 避免在
__init__中做耗时操作,比如打开文件、连数据库——那会发生在装饰阶段,而非运行阶段 - 如果需要全局共享缓存,得用模块级变量或单例,而不是依赖
self
调试时怎么确认装饰器真的生效了?
别只看函数是否运行,重点查三件事:参数是否传进来了、wrapper 是否被调用、原函数属性是否保留。
加一行 print(f"Using level={level}") 在外层函数里,启动时就会打印;在 wrapper 里加 print("Running..."),每次调用才触发。再检查:
def hello(): return "hi" decorated = retry(max_attempts=1)(hello) print(decorated.__name__) # 应该是 "hello",不是 "wrapper"
- 如果
__name__是wrapper,说明漏了@wraps或位置不对 - 如果装饰器参数打印多次,说明外层函数被重复调用——可能误写成
@retry(没括号)导致 Python 把函数当参数传给了外层 - 用
help(decorated)看 docstring 是否保留,这是@wraps最容易被忽略的验证点
复杂装饰器的坑往往不在语法,而在闭包变量捕获时机和作用域混淆——写完务必用不同参数组合跑几遍真实调用。