Python闭包与装饰器的高级陷阱 Python闭包与装饰器的高级陷阱闭包看似简单但实际使用中隐藏着大量陷阱。先看一个常见的问题def make_counters():counters []for i in range(5):def counter():return icounters.append(counter)return countersfor c in make_counters():print(c())输出结果是4, 4, 4, 4, 4而不是0, 1, 2, 3, 4。原因在于闭包捕获的是变量i的引用而不是i的值。当counter被调用时for循环已经执行完毕i的值是4。解决方法是利用默认参数在定义时绑定值def make_counters():counters []for i in range(5):def counter(ii):return icounters.append(counter)return counters默认参数在函数定义时求值ii把当前值绑定到默认参数上。但这种方式有个隐患如果有人调用counter()时传了参数就会覆盖默认值。更安全的方式是用闭包工厂def make_counters():counters []for i in range(5):def make_counter(val):def counter():return valreturn countercounters.append(make_counter(i))return counters每次调用make_counter(i)都创建一个新的作用域val被绑定到传入的值上不再受后续循环影响。装饰器的参数传递陷阱更隐蔽。看这个例子def timer(func):import timedef wrapper(*args, **kwargs):start time.perf_counter()result func(*args, **kwargs)print(f{func.__name__} took {time.perf_counter() - start:.4f}s)return resultreturn wrappertimerdef process(data, timeout30):pass表面上看没问题但wrapper的签名丢失了。inspect.signature(process)返回的是(*args, **kwargs)而不是(data, timeout30)。使用functools.wraps可以修复from functools import wrapsdef timer(func):import timewraps(func)def wrapper(*args, **kwargs):...return wrapperfunctools.wraps把原函数的__name__、__qualname__、__doc__、__dict__、__module__、__annotation__等属性复制到wrapper上。但它不修复签名inspect.signature仍然返回错误的签名。完整的修复需要from functools import wrapsimport inspectdef timer(func):wraps(func)def wrapper(*args, **kwargs):...wrapper.__signature__ inspect.signature(func)return wrapper带参数的装饰器需要三层嵌套def retry(max_attempts3, delay1):def decorator(func):wraps(func)def wrapper(*args, **kwargs):for attempt in range(max_attempts):try:return func(*args, **kwargs)except Exception as e:if attempt max_attempts - 1:raisetime.sleep(delay)return wrapperreturn decoratorretry(max_attempts5, delay2)def fetch_data(url):passretry() # 注意括号不能省略def fetch_other(url):passretry()必须加括号即使使用默认参数。因为retry是一个返回装饰器的函数不是装饰器本身。如果写成retry而不是retry()Python会把retry当作装饰器调用把被装饰函数作为max_attempts参数传进去。一个技巧是判断第一个参数是否可调用def retry(funcNone, max_attempts3, delay1):if func is not None:wraps(func)def wrapper(*args, **kwargs):...return wrapperelse:def decorator(func):wraps(func)def wrapper(*args, **kwargs):...return wrapperreturn decoratorretrydef foo(): pass # 无参数retry(max_attempts5) # 有参数def bar(): pass这个模式的原理是如果retry后面没有括号Python直接把被装饰函数作为func传入如果有括号func为None进入else分支返回真正的装饰器。类装饰器的self绑定问题class Logger:def __init__(self, func):self.func funcdef __call__(self, *args, **kwargs):print(fCalling {self.func.__name__})return self.func(*args, **kwargs)Loggerdef compute(x, y):return x ycompute(1, 2) # 正常class MyClass:Loggerdef method(self):return 42MyClass().method() # 会出错method被装饰后变成了Logger的实例。当通过实例访问时Python不会自动传入self。需要手动处理描述符协议class Logger:def __init__(self, func):self.func funcdef __call__(self, *args, **kwargs):print(fCalling {self.func.__name__})return self.func(*args, **kwargs)def __get__(self, obj, objtypeNone):if obj is None:return selfimport functoolsreturn functools.partial(self.__call__, obj)通过实现__get__方法Logger变成了描述符。当通过实例访问被装饰的方法时__get__被调用返回绑定好self的partial对象。装饰器的堆叠顺序也很重要logvalidatecachedef expensive(x):return complex_calc(x)等价于expensive log(validate(cache(expensive)))。执行顺序是从下到上装饰从上到下执行。调用expensive(x)时先执行log再执行validate最后执行cache。如果装饰器之间相互依赖顺序错误会导致灾难。比如login_required必须在route之后因为route可能改变了函数的URL路由信息。闭包的内存泄漏是另一个常见问题def create_report():large_data load_terabytes_of_data()def generate():return process(large_data)return generatereporter create_report()reporter持有对large_data的引用即使generate函数不直接使用large_data。在CPython中闭包捕获了整个外层作用域的变量集合。__closure__属性中包含了所有被引用的自由变量print(reporter.__closure__)如果large_data在generate中从未被使用但它定义在同一个作用域中仍然会被捕获。解决方法是在不需要时删除引用def create_report():large_data load_terabytes_of_data()result process(large_data)del large_data # 提前释放def generate():return resultreturn generate装饰器堆叠过多时的问题decorator1decorator2decorator3decorator4decorator5def deeply_decorated():pass调用deeply_decorated时的函数调用栈深度等于装饰器层数加一。如果每个装饰器都添加了try/except或日志堆栈跟踪会变得极其混乱。解决方式是使用装饰器压缩技术def compose(*decorators):def deco(func):for decorator in reversed(decorators):func decorator(func)return funcreturn decocompose(decorator1, decorator2, decorator3, decorator4, decorator5)def deeply_decorated():passcompose把所有装饰器合并成一个减少了调用栈深度。更严重的是循环导入问题。当装饰器定义在另一个模块中而被装饰函数所在的模块也被装饰器模块导入时# decorators.pyfrom .utils import helperdef log(func):wraps(func)def wrapper(*args, **kwargs):helper() # 使用utils的功能return func(*args, **kwargs)return wrapper# utils.pyfrom .decorators import loglogdef helper():pass导入utils时Python尝试导入decorators导入decorators时Python又尝试导入utils。此时utils尚未完全加载log装饰器执行时导致ImportError。解决方式是延迟导入# decorators.pydef log(func):wraps(func)def wrapper(*args, **kwargs):from .utils import helper # 延迟导入helper()return func(*args, **kwargs)return wrapper或者使用forward reference模式把装饰器移到独立的模块中让装饰器模块只依赖基础工具不依赖业务模块。这些陷阱在真实项目中几乎都会遇到。理解闭包和装饰器的底层机制是绕过这些陷阱的关键。