You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

装饰可调用类时的属性访问问题及指数退避装饰需求

解决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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 08:36:38