Python 中的装饰器传参三层嵌套
为什么装饰器需要传参?
装饰器是 Python 中用来修改函数行为的强大工具。大多数初学者最先接触到的是无参装饰器,它们直接“包裹”目标函数。但实际项目中,我们常常希望装饰器本身能接受参数,以便控制装饰逻辑,例如限制重试次数、设置超时时间或指定日志级别。
此时,简单的两层嵌套就不再适用了,必须引入第三层嵌套。这一层用于接收装饰器的配置参数,并返回一个真正的装饰器函数。
理解这种结构的关键在于:@decorator 语法本质上是一个函数调用。
当装饰器带参数时,@decorator(args) 首先被求值,它必须返回一个标准的无参装饰器,然后再去包裹目标函数。
从无参装饰器回顾
先看一个最简单的无参装饰器,它统计函数运行时间:
import time
def timer(func): # 第一层:接收函数
def wrapper(*args, **kwargs): # 第二层:包裹逻辑
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} 运行耗时: {end - start:.4f}s")
return result
return wrapper
@timer
def sleep_abit():
time.sleep(0.5)
print("醒来")
sleep_abit()
输出:
醒来
sleep_abit 运行耗时: 0.5005s
在这个过程中,@timer 等价于 sleep_abit = timer(sleep_abit)。只有两层函数:外层 timer 接收函数,内层 wrapper 是实际调用的新函数。
有参装饰器的三层嵌套结构
如果我们需要让 timer 接受一个参数,比如指定时间单位(秒或毫秒),或者决定是否打印日志,就要改造为三层嵌套。
架构图示
@decorator(param)
│
└──> 1. 调用 decorator(param) → 返回真正的装饰器 real_decorator
2. 然后把目标函数传递给 real_decorator(func)
3. real_decorator(func) 返回最终的 wrapper 函数
用伪代码表示就是:
def decorator(param): # 最外层:接收装饰器参数
def actual_decorator(func): # 中间层:接收函数(这才是真正的装饰器)
def wrapper(*args, **kwargs): # 最内层:新功能包裹层
# 可以使用 param 和 func
result = func(*args, **kwargs)
return result
return wrapper
return actual_decorator # 返回装饰器
完整实例:带精度控制的计时器
我们实现一个 timer 装饰器,可以接受参数 precision 控制时间打印的小数位数,以及一个布尔值 log 决定是否输出。
import time
from functools import wraps
def timer(precision=4, log=True):
"""最外层:接收装饰器参数"""
def decorator(func):
"""中间层:真正的装饰器,接收原函数"""
@wraps(func)
def wrapper(*args, **kwargs):
"""最内层:包裹函数,实现计时逻辑"""
start = time.time()
result = func(*args, **kwargs)
end = time.time()
if log:
time_str = f"{end - start:.{precision}f}"
print(f"[计时] {func.__name__} 耗时: {time_str}s")
return result
return wrapper
return decorator
# 使用带参数的装饰器
@timer(precision=6, log=True)
def fast_calc():
return sum(range(100000))
@timer(precision=2, log=True)
def slow_calc():
time.sleep(1.2)
return 42
print(fast_calc())
print(slow_calc())
输出示例:
[计时] fast_calc 耗时: 0.002456s
4999950000
[计时] slow_calc 耗时: 1.20s
42
请注意:
timer(precision=6, log=True)得到的是一个装饰器decorator,然后decorator(fast_calc)返回wrapper。- 我们使用了
functools.wraps保持原函数的元信息(如__name__、__doc__)。 - 最内层
wrapper可以自由访问precision和log,它们是闭包变量。
逐层拆解执行流程
对于 @timer(log=False) 修饰的函数:
- Python 解释器遇到
@timer(log=False)
先计算timer(log=False),调用最外层函数,返回decorator函数对象。 - 用返回的
decorator包裹下一行的函数
这相当于执行decorator(target_function),decorator接收函数并返回wrapper。 - 将原函数名重新绑定到
wrapper
当用户调用target_function(...)时,实际执行的是wrapper(...)。
图解代码化:
# @timer(log=False) 底层等价于:
def some_func():
pass
some_func = timer(log=False)(some_func)
timer(log=False) 是第一次调用,返回 decorator;再传入 some_func 得到 wrapper。
三层嵌套中的重要细节
1. 保持函数签名正确
使用 functools.wraps(而不是手动复制 __name__)是强烈推荐的做法。它能让 help()、文档生成工具和内省机制正常工作。
from functools import wraps
def my_decorator(arg1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# ...
return func(*args, **kwargs)
return wrapper
return decorator
2. 参数的默认值
三层嵌套让装饰器具有默认参数成为可能,这样调用时可以省略括号:
def repeat(times=2): # 默认重复2次
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat() # 使用默认值2,括号不能省略(除非你再做特殊处理)
def greet():
print("你好")
@repeat(times=3) # 显式指定3次
def attention():
print("重要的事情说三遍")
如果希望 @repeat 不带括号也能工作,通常需要把装饰器写成一个能同时处理“无括号”和“有括号”的单一函数,但这样会引入更多复杂性,初学者现阶段应优先掌握清晰的三层形式。
3. 多个装饰器参数
三层嵌套很容易扩展:最外层函数可以接收任意参数和关键字参数。
def log_info(level="INFO", prefix="LOG"):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[{prefix}][{level}] 调用 {func.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
常见误区与调试技巧
-
忘记返回中间层装饰器
最外层函数必须返回内部装饰器,否则会得到NoneType错误。 -
混淆各层职责
最外层只负责接收配置参数;中间层才是真正的装饰器,它接收函数;最内层是具体的包裹逻辑。 -
闭包变量引用问题
在wrapper中引用的外部变量(如precision)会被妥善保存在闭包中,但注意不要错误地修改不可变类型后仍期望外层生效。通常只是读取它们,所以很安全。 -
如何快速验证结构?
可以在每一层添加print观察调用顺序:def test_dec(arg): print(f"最外层被调用, arg={arg}") def dec(func): print(f"中间层被调用, func={func.__name__}") @wraps(func) def wrapper(*args, **kwargs): print("最内层包裹函数执行") return func(*args, **kwargs) return wrapper return dec修饰一个函数时,会立即看到前两层输出,而最内层输出只在函数实际调用时出现。
实战:可配置的重试装饰器
下面是一个完整的重试装饰器,接受重试次数和异常类型作为参数,具备实际应用价值。
import time
from functools import wraps
def retry(max_retries=3, delay=0, exceptions=(Exception,)):
"""
可传参的重试装饰器
:param max_retries: 最大重试次数
:param delay: 每次重试间隔(秒)
:param exceptions: 需要重试的异常类型元组
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_retries + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
if attempt < max_retries:
print(f"重试 {attempt+1}/{max_retries},原因: {e}")
time.sleep(delay)
# 所有重试耗尽
raise last_exception
return wrapper
return decorator
# 使用示例
import random
@retry(max_retries=2, delay=0.5, exceptions=(ValueError,))
def unstable_operation():
if random.random() < 0.7:
raise ValueError("随机失败")
return "成功"
try:
print(unstable_operation())
except ValueError as e:
print(f"最终失败: {e}")
这个例子中,retry(max_retries=2, delay=0.5, exceptions=(ValueError,)) 清晰地展示了三层嵌套如何将配置参数牢牢锁定在闭包中,所有重试逻辑完全隔离在装饰器内,业务函数无需关心。
总结
- 当装饰器自身需要参数时,必须采用三层函数嵌套。
- 最外层负责接收参数并返回真正的装饰器;中间层是标准的无参装饰器,接收原函数;最内层是实现增强功能的包裹函数。
@decorator(args)语法会先计算decorator(args),其返回值必须是一个装饰器函数。- 始终使用
functools.wraps保留原函数的元数据。 - 多练习写几个有参装饰器(计时、重试、权限检查、日志),三层结构会变得非常自然。
掌握有参装饰器的三层嵌套,你就彻底理解了 Python 装饰器的内部机制,并为编写更高级的元编程模式打下了坚实基础。