装饰器与闭包

What — 是什么

闭包(Closure)是函数与其引用环境的组合体;装饰器(Decorator)是基于闭包的语法糖,用于在不修改原函数的前提下增强其功能。

核心概念:

  • 闭包:内部函数引用外部函数的变量,外部函数返回内部函数后,变量仍被保留
  • 装饰器@decorator 语法等价于 func = decorator(func)
  • functools.wraps:保留被装饰函数的元信息(__name____doc__

关键特性:

  • 装饰器本质是高阶函数(接收函数,返回函数)
  • 支持叠加使用,顺序从下到上执行
  • 可带参数(三层嵌套)

运行机制:

  • 内存模型:闭包变量存储在函数的 __closure__ 属性中(cell 对象)
  • 执行模型:装饰器在模块加载时执行(定义阶段),不是调用阶段
  • 并发模型:装饰器本身无线程安全问题,但闭包变量被多线程共享时需注意

类型系统:

  • 类型分类:Callable 类型,可使用 ParamSpecTypeVar 做泛型装饰器
  • 类型转换规则:不适用
  • 泛型/多态支持: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))记住:离函数近的先执行
类方法装饰器实例方法第一个参数是 selfwrapper 签名用 *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) 控制装饰器行为(@cachecache.clear());2) 标记函数(@api_endpointfunc.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__ 解析真实函数签名。


相关链接: