装饰可调用类时的属性访问问题及指数退避装饰需求
解决backoff装饰可调用类后的属性访问问题
当你用backoff的on_exception装饰可调用类的实例时,会遇到属性访问失效的问题——这是因为装饰器返回的是一个包装函数对象,而非原来的CallMeMaybe实例,所以直接访问包装后的对象的属性(比如n_calls)会抛出AttributeError。下面给你几种可行的解决方案:
方案1:装饰类的__call__方法(最推荐)
直接在类内部的__call__方法上应用装饰器,这样实例化后的对象依然是CallMeMaybe的实例,属性可以正常访问。
from backoff import on_exception, expo class CallMeMaybe: __name__ = 'maybe' def __init__(self): self.n_calls = 0 # 直接装饰__call__方法 @on_exception(expo, Exception, max_tries=3) def __call__(self): self.n_calls += 1 raise Exception # 验证效果 f = CallMeMaybe() print(f.n_calls) # 输出:0 for i in range(7): try: f() except Exception: pass print(f.n_calls) # 输出:21(注意:每次调用f()会触发3次重试,所以7次调用累计执行了21次__call__)
⚠️ 注意:如果你的需求是统计用户发起的调用次数(而非重试后的总执行次数),需要把n_calls的递增逻辑移到重试之外,比如:
class CallMeMaybe: __name__ = 'maybe' def __init__(self): self.user_calls = 0 self.total_executions = 0 def __call__(self): self.user_calls += 1 # 把需要重试的逻辑单独封装成方法 self._execute_with_retry() @on_exception(expo, Exception, max_tries=3) def _execute_with_retry(self): self.total_executions += 1 raise Exception
方案2:保留原实例的引用
如果你必须装饰实例而非类方法,可以单独保存原实例的引用,通过原实例访问属性:
from backoff import on_exception, expo class CallMeMaybe: __name__ = 'maybe' def __init__(self): self.n_calls = 0 def __call__(self): self.n_calls += 1 raise Exception # 实例化原对象 original_f = CallMeMaybe() # 装饰实例 decorated_f = on_exception(expo, Exception, max_tries=3)(original_f) # 验证效果 print(original_f.n_calls) # 输出:0 for i in range(7): try: decorated_f() except Exception: pass print(original_f.n_calls) # 输出:21
方案3:创建代理装饰器,让包装对象支持属性访问
如果你希望装饰后的对象能像原实例一样直接访问属性,可以写一个简单的类装饰器,把原实例的属性代理到包装函数上:
from backoff import on_exception, expo import functools def backoff_wrap_class(cls): @functools.wraps(cls) def wrapper(*args, **kwargs): # 创建原实例 instance = cls(*args, **kwargs) # 装饰实例的__call__行为 decorated_call = on_exception(expo, Exception, max_tries=3)(instance) # 将原实例的属性复制到包装函数上 functools.update_wrapper(decorated_call, instance) # 额外保存原实例引用,方便后续直接访问 decorated_call._original = instance return decorated_call return wrapper @backoff_wrap_class class CallMeMaybe: __name__ = 'maybe' def __init__(self): self.n_calls = 0 def __call__(self): self.n_calls += 1 raise Exception # 验证效果 f = CallMeMaybe() print(f.n_calls) # 输出:0(直接访问包装对象的属性) for i in range(7): try: f() except Exception: pass print(f.n_calls) # 输出:21 # 也可以通过_original访问原实例 print(f._original.n_calls) # 输出:21
内容的提问来源于stack exchange,提问作者wim
相关产品推荐
相关产品推荐

