bigywater.tech
Python · 10

__getattribute__ 和 __getattr__:属性查找失败后发生了什么

统计__getattribute__和__getattr__的调用次数,验证前者拦截每一次属性访问、后者只在查找失败时兜底,并复现一个经典的无限递归陷阱。

8次 vs 3次
同样8次访问,两个方法的调用次数
总入口 vs 兜底
__getattribute__拦截一切,__getattr__只管失败
RecursionError
复现的经典无限递归陷阱

做的事:给两个方法都装上计数器,看谁被调用了几次

__getattribute__理论上"每次属性访问都会触发",__getattr__理论上"只有找不到时才触发"。

代码class Counter: def __getattribute__(self, name): self.calls_getattribute += 1 # 记录每次调用 return object.__getattribute__(self, name) def __getattr__(self, name): self.calls_getattr += 1 # 记录兜底被触发次数 raise AttributeError(name) c = Counter() c.existing_field = 1 for _ in range(5): _ = c.existing_field # 访问存在的属性 for _ in range(3): try: _ = c.missing_field # 访问不存在的属性 except AttributeError: pass

验证:调用次数精确对应的访问模式

5次访问存在的属性 + 3次访问不存在的属性。

结果调用__getattribute__次数: 8 调用__getattr__次数: 3

数字精确吻合预期:__getattribute__ 被调用了8次——5次存在的 + 3次不存在的,每一次 属性访问,不管最后成不成功,都要先经过它。__getattr__ 只被调用了3次——正好对应那3次真正查找失败的访问。

这就是两者的分工:__getattribute__ 是属性访问的总入口,拦截一切(包括方法调用、存在的属性);__getattr__ 只是一个兜底,只有当正常的查找流程(实例字典、类字典、父类)都没找到时,才会被唤起,给你一次"最后挽救"的机会。绝大多数场景下自定义 __getattr__ 就够用(性能开销小,只在真正需要时触发),重写 __getattribute__ 则要小心,因为它拦截了一切访问,写不好很容易拖慢性能甚至出bug。

验证:__getattr__里的一个经典陷阱

如果在__getattr__内部又去访问self的另一个属性,会发生什么。

代码class Trap: def __getattr__(self, name): return self.backup_dict[name] # 想读一个"备用字典" trap = Trap() trap.anything 结果: RecursionError: maximum recursion depth exceeded

这行代码想表达"如果没找到属性,就去 self.backup_dict 里找",但 Trap 这个类本身压根没有定义过 backup_dict 这个属性——所以访问 self.backup_dict 本身,又会因为找不到而再次触发 __getattr__,而这次触发又会再去访问一次 self.backup_dict……无限循环下去,直到Python的递归深度上限,抛出 RecursionError。

这是写 __getattr__ 时最容易踩的坑:在它内部访问 self 的其它属性时,一定要确认那个属性是"正常能找到的"(比如已经在 __init__ 里赋过值,存在于实例字典),不然一旦那个属性本身也找不到,就会触发同一个 __getattr__ 再次执行,陷入无限递归。

分步演示:两个方法的调用次数与陷阱

每一步都是本文上面用Python 3.14跑出来的原始结果。

__getattribute__拦截一切,__getattr__只是失败后的兜底
这一步新验证的结果

    本文用到的工具

    • 本机 Python 3.14.7
    • 自定义 __getattribute__/__getattr__ 计数器:统计调用次数
    京ICP备2026067502号-1京公网安备11010502063487号