装饰器与闭包
What — 是什么
闭包(Closure)是函数与其引用环境的组合体;装饰器(Decorator)是基于闭包的语法糖,用于在不修改原函数的前提下增强其功能。
核心概念:
- 闭包:内部函数引用外部函数的变量,外部函数返回内部函数后,变量仍被保留
- 装饰器:
@decorator语法等价于func = decorator(func) functools.wraps:保留被装饰函数的元信息(__name__、__doc__)
关键特性:
- 装饰器本质是高阶函数(接收函数,返回函数)
- 支持叠加使用,顺序从下到上执行
- 可带参数(三层嵌套)
运行机制:
- 内存模型:闭包变量存储在函数的
__closure__属性中(cell 对象) - 执行模型:装饰器在模块加载时执行(定义阶段),不是调用阶段
- 并发模型:装饰器本身无线程安全问题,但闭包变量被多线程共享时需注意
类型系统:
- 类型分类:
Callable类型,可使用ParamSpec和TypeVar做泛型装饰器 - 类型转换规则:不适用
- 泛型/多态支持:Python 3.10+ 可用
ParamSpec做通用装饰器类型
Why — 为什么
适用场景:
- 日志记录
- 权限验证
- 缓存(
@lru_cache) - 重试机制
- 计时/性能监控
对比其他语言:
| 维度 | Python 装饰器 | Java 注解 | Go 函数选项 |
|---|---|---|---|
| 性能 | 函数调用开销 | 编译时处理 | 无额外开销 |
| 生态 | 标准库丰富(functools) | 框架驱动 | 手写 |
| 上手难度 | 低 | 中 | 中 |
| 灵活性 | 极高(运行时修改) | 中(编译时绑定) | 中 |
优缺点:
- ✅ 优点:
- 语法优雅,声明式增强函数
- 复用性强,一个装饰器可装饰多个函数
- 不修改原函数代码,符合开放封闭原则
- ❌ 缺点:
- 调试困难(调用栈多一层包装)
- 装饰器叠加时执行顺序不直观
- 忘记
@wraps会丢失函数元信息
How — 怎么用
快速上手
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
import time
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} 耗时 {time.time() - start:.3f}s")
return result
return wrapper
@timer
def slow_func():
import time
time.sleep(1)
slow_func() # slow_func 耗时 1.001s
代码示例
带参数的装饰器:
def retry(max_attempts=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
raise
import time
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=5, delay=2)
def call_api():
# 可能失败的外部调用
pass
类装饰器:
class Singleton:
def __init__(self, cls):
self._cls = cls
self._instance = None
def __call__(self, *args, **kwargs):
if self._instance is None:
self._instance = self._cls(*args, **kwargs)
return self._instance
@Singleton
class Database:
def __init__(self):
self.connected = True
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 函数元信息丢失 | 装饰器返回 wrapper,元信息是 wrapper 的 | 使用 @functools.wraps(func) |
| 装饰器顺序混淆 | @a @b def f 等价于 f = a(b(f)) | 记住:离函数近的先执行 |
| 类方法装饰器 | 实例方法第一个参数是 self | wrapper 签名用 *args, **kwargs |
| 闭包变量延迟绑定 | 循环中闭包引用循环变量 | 用默认参数捕获 def f(var=i) |
最佳实践
- 始终使用
@functools.wraps(func)保留元信息 - 复杂装饰器用类实现(
__call__),更易维护 - 装饰器保持单一职责
- 用
__wrapped__属性访问原始函数
面试题
Q1: 什么是闭包?闭包的原理是什么?
闭包是内部函数引用了外部函数的变量,且外部函数已经返回,但被引用的变量仍然存活的机制。原理上,被引用的变量存储在函数的
__closure__属性(cell 对象)中,不会随外部函数栈帧销毁而被回收。
Q2: 多个装饰器叠加时的执行顺序是什么?
装饰器从下到上(离函数近的先)执行包装,调用时从外到内执行。
@a @b def f等价于f = a(b(f)),调用f()时先进入a的 wrapper,再进入b的 wrapper,最后执行原函数。
Q3: 带参数的装饰器为什么要用三层嵌套?
最外层接收装饰器参数,中间层接收被装饰函数,最内层是实际执行的 wrapper。因为
@retry(max_attempts=3)先调用retry(3)返回装饰器函数,再对目标函数执行装饰,所以需要三层:参数层 → 装饰器层 → wrapper 层。
Q4: functools.wraps 的作用是什么?不使用会有什么问题?
@wraps(func)将原函数的__name__、__doc__、__module__等元信息复制到 wrapper 函数上。不使用时,被装饰函数的元信息会被 wrapper 覆盖,导致调试困难、文档丢失、基于函数名的路由/序列化出错。
Q5: 闭包在循环中引用循环变量会有什么问题?如何解决?
闭包延迟绑定循环变量,所有闭包最终引用的是循环结束后的最后一个值。解决方法:用默认参数捕获当前值
def f(var=i),或用functools.partial固定参数。
Q6: 装饰器和继承(子类覆盖)有什么区别?什么时候用哪个?
装饰器是组合模式,不修改原函数代码,运行时动态增强,可叠加多个,可随时移除。继承是子类覆盖父类方法,编译时确定,修改原始类定义。装饰器适合横切关注点(日志/缓存/权限),继承适合”是一个”关系的类型扩展。优先组合(装饰器),继承用于真正的 is-a 关系。
Q7: 如何给装饰器添加属性?有什么用途?
装饰器返回的 wrapper 函数可以添加属性:
wrapper._cache = {}、wrapper.enabled = True。用途:1) 控制装饰器行为(@cache的cache.clear());2) 标记函数(@api_endpoint的func.is_api = True);3) 存储装饰器状态(@rate_limit的计数器)。functools.wraps会保留原函数属性,自定义属性在 wraps 之后添加。
Q8: __wrapped__ 属性有什么作用?如何利用它?
@functools.wraps(func)会将原函数存储在 wrapper 的__wrapped__属性中。用途:1) 访问原始函数——func.__wrapped__()绕过装饰器直接调用;2) 内省——检查函数签名、源码时不被 wrapper 干扰;3) 测试——mock 原始函数而非装饰后的版本;4) 框架路由——Django/FastAPI 基于__wrapped__解析真实函数签名。
相关链接: