setattr 如何避免无限递归的防护写法

setattr在自定义__setattr__中触发无限递归,因所有属性赋值均调用该方法;安全做法是直接操作self.__dict__[name] = value(无__slots__时)或调用object.__setattr__(self, name, value)。

为什么 setattr 在自定义 __setattr__ 里会触发无限递归

当你在类的 __setattr__ 方法内部直接调用 setattr(self, name, value),Python 会再次进入同一个 __setattr__,形成死循环。这不是 bug,而是机制:所有属性赋值(包括 self.x = ysetattr(self, ...))都会走 __setattr__

标准防护写法:绕过自定义逻辑,直写实例字典

最常用、最安全的方式是跳过 __setattr__,把值直接写进实例的 __dict__

def __setattr__(self, name, value):
    if name == "special_flag":
        # 自定义逻辑
        super().__setattr__(name, value.upper())  # 或其他处理
    else:
        # 防护:不触发 __setattr__,避免递归
        self.__dict__[name] = value
  • self.__dict__[name] = value 是底层字典操作,完全绕过 __setattr__
  • 适用于大多数普通属性;但对使用 __slots__ 的类不生效(此时需用 object.__setattr__(self, name, value)
  • 注意:如果类有 property 或描述符,这种写法会跳过它们的 setter,只写入实例字典 —— 这正是你想要的「绕过」行为

替代方案:用 object.__setattr__ 显式委托

当类继承链复杂,或你希望保持与父类 __setattr__ 行为一致(比如父类做了验证),应显式调用基类实现:

def __setattr__(self, name, value):
    if name == "counter":
        if not isinstance(value, int):
            raise TypeError("counter must be int")
        # 委托给 object.__setattr__,不触发当前类重写的 __setattr__
        object.__setattr__(self, name, value)
    else:
        super().__setattr__(name, value)  # 或继续委托给父类
  • object.__setattr__(self, name, value) 是 Python 属性赋值的最终实现,不经过任何自定义 __setattr__
  • self.__dict__ 更通用:兼容 __slots__、描述符、property setter(只要它们没被覆盖)
  • 不要写成

    super().__setattr__(...) —— 如果父类也重写了 __setattr__,仍可能递归

容易踩的坑:你以为绕过了,其实没绕开

以下写法看似“手动”,实则仍在触发 __setattr__

  • self.name = value → 触发当前 __setattr__
  • setattr(self, name, value) → 触发当前 __setattr__
  • self.__dict__.update({name: value}) → 看似字典操作,但某些情况下(如 __dict__ 被代理或封装)可能间接触发
  • __init__ 中未做防护就调用 setattr → 同样递归,因为 __init__self.x = y 也会走 __setattr__

真正安全的只有两种:直接操作 self.__dict__(无 __slots__ 时),或明确调用 object.__setattr__。其余都是幻觉。