同一类装饰器装饰嵌套调用函数与方法报错的原因及解决方案
我刚开始接触装饰器,在两个存在嵌套调用关系的函数上使用同一个类装饰器时遇到了运行错误。如下是参考Stack Overflow相关问题实现的装饰器代码:
class PrintEvent(): def __init__(self, fn=None): self.fn=fn def __call__(self, *args, **kwargs): try: out=self.fn(self.instance, *args, **kwargs) print(f'Function {self.fn.__name__} executed succesfully.') except: out=None print(f'Function {self.fn.__name__} failed: {traceback.format_exc()}') return out def __get__(self, instance, owner=None): if instance is None: return self self.instance=instance return self
使用该装饰器编写的测试代码如下:
@PrintEvent def foo_function(number): return number * 2 class FooClass: def __init__(self, var): self.var=var @PrintEvent def foo_method(self): output=foo_function(self.var) return output
装饰器PrintEvent同时装饰了全局函数foo_function和FooClass类的方法foo_method,执行如下测试代码时:
if __name__=='__main__': test=FooClass(2) test.foo_method()
得到如下输出:
Function foo_function failed: Traceback (most recent call last): File "c:\xxxx...", line 11, in __call__ out=self.fn(self.instance, *args, **kwargs) AttributeError: 'PrintEvent' object has no attribute 'instance' Function foo_method executed succesfully.
根据报错回溯信息可知,调用foo_method时会触发PrintEvent.__get__方法,但调用全局函数foo_function时不会触发该方法,导致后者对应的装饰器实例未生成instance属性,最终抛出错误。
原本对这段代码执行逻辑的理解为:PrintEvent.__call__(FooClass.foo_method(PrintEvent.__call__(foo_function)))
现有三个疑问:
- 为什么
PrintEvent.__call__方法共被调用两次,但PrintEvent.__get__方法仅被触发一次? - 该问题有什么可行的规避/修复方案?
- 对当前实现的装饰器结构有什么优化建议?
1. 为什么__get__只触发一次
__get__是Python描述符协议的内置方法,只有当装饰器实例作为类属性存在时,通过类实例/类本身访问这个属性才会触发__get__逻辑:
- 被装饰的
foo_method是FooClass的类属性,执行test.foo_method()时,会先通过类实例test访问到这个PrintEvent实例,自动触发__get__把当前类实例绑定到self.instance,这是唯一一次__get__调用。 - 被装饰的
foo_function是全局作用域的独立函数,不属于任何类的属性,访问它的时候不会走描述符触发逻辑,自然永远不会执行__get__,它对应的PrintEvent实例从来没有被赋值过instance属性,__call__里强行访问self.instance就会抛出属性错误。
另外之前对执行逻辑的理解存在偏差:嵌套调用是发生在foo_method的函数体内部,不是装饰器套娃调用。实际执行顺序是:test.foo_method触发__get__拿到绑定好实例的装饰器对象,调用这个对象触发__call__,__call__内部执行原foo_method逻辑,走到foo_function(self.var)这一行时,才会去调用全局的foo_function对应的装饰器实例,触发它的__call__。
2. 可行的修复方案
核心修复逻辑是:区分被装饰对象是普通函数还是类方法,不要强行给普通函数传self.instance参数。
最直接的修复方式是在__call__里判断当前实例有没有绑定类实例,没有就说明装饰的是普通函数,直接传参调用即可:
import traceback class PrintEvent(): def __init__(self, fn=None): self.fn=fn def __call__(self, *args, **kwargs): try: # 绑定了类实例就作为第一个参数传入,没有就直接调用原函数 if hasattr(self, 'instance'): out = self.fn(self.instance, *args, **kwargs) else: out = self.fn(*args, **kwargs) print(f'Function {self.fn.__name__} executed successfully.') except: out=None print(f'Function {self.fn.__name__} failed: {traceback.format_exc()}') return out def __get__(self, instance, owner=None): if instance is None: return self # 不要修改原装饰器实例的属性,每次访问返回新的绑定对象,避免多实例下的状态污染 bound_decorator = PrintEvent(self.fn) bound_decorator.instance = instance return bound_decorator
修改后再运行测试代码,两个函数都能正常执行,不会再抛出属性错误。
3. 装饰器结构优化建议
- 修复多实例状态污染问题:原实现的
__get__直接给装饰器实例本身赋值self.instance,如果同一个类生成多个不同实例,先后调用被装饰方法会互相覆盖instance值,多线程、嵌套调用场景下会出现难以排查的逻辑错误。正确做法是每次__get__触发时,生成一个新的绑定了当前实例的装饰器对象返回,不要修改全局共享的原装饰器实例状态。 - 避免裸异常捕获:原实现用裸
except:会捕获所有异常,包括KeyboardInterrupt、SystemExit这类本不该被业务逻辑拦截的系统级异常,建议改成except Exception:,仅捕获常规业务异常。 - 保留原函数元信息:原实现装饰后原函数的函数名、文档字符串、参数签名等元信息会被替换成装饰器实例的信息,调试、文档生成时会出问题,可以用
functools.update_wrapper把原函数的元信息拷贝到装饰器实例上。 - 扩展装饰器参数支持:可以补全初始化逻辑,让装饰器既支持直接
@PrintEvent使用,也支持带参数@PrintEvent(catch_exception=False)的使用场景,灵活性更高。 - 补全缺失依赖:原代码使用了
traceback.format_exc()但没有导入traceback模块,直接运行会抛出NameError,需要在文件头部补充导入语句。
优化后的完整参考实现:
import traceback import functools class PrintEvent(): def __init__(self, fn=None, *, catch_exception=True): # 接收装饰器自定义参数 self.catch_exception = catch_exception self.instance = None self.fn = None if fn is not None: # 拷贝原函数元信息 functools.update_wrapper(self, fn) self.fn = fn def __call__(self, *args, **kwargs): # 处理带参数装饰器的场景:先接收被装饰函数 if self.fn is None: self.fn = args[0] functools.update_wrapper(self, self.fn) return self try: if self.instance is not None: out = self.fn(self.instance, *args, **kwargs) else: out = self.fn(*args, **kwargs) print(f'Function {self.fn.__name__} executed successfully.') return out except Exception as e: if not self.catch_exception: raise out = None print(f'Function {self.fn.__name__} failed: {traceback.format_exc()}') return out def __get__(self, instance, owner=None): if instance is None: return self # 每次返回新的绑定实例,避免跨实例状态污染 bound_decorator = PrintEvent(self.fn, catch_exception=self.catch_exception) bound_decorator.instance = instance return bound_decorator
这个版本既支持装饰普通函数、类实例方法,也支持给装饰器传参,不会有跨实例状态污染问题,也不会误吞系统级异常。
内容的提问来源于stack exchange,提问作者Dasi

