你的属性访问为何陷入无限递归——Python__getattr__与__getattribute__的致命区别与掌控秘籍在 Python 的面向对象编程中__getattr__和__getattribute__是两个无比强大的魔方法它们让你能完全控制属性的访问行为。你可以用它们实现“懒加载”、“属性代理”、“虚拟属性”、“日志记录”等高级特性。然而许多开发者在这里跌进一个万劫不复的深渊无限递归。程序在瞬间耗尽调用栈抛出RecursionError而你甚至找不到到底哪里引发了递归。更隐蔽的是有时候递归发生在特定条件才触发测试时一切正常上线后突然崩溃。这一切都源于对这两个方法的根本性误解它们虽然名字相似但触发时机、职责边界和必须遵守的安全规则截然不同。今天我们就来彻底解剖这对“属性访问双雄”的底层机制揭开那些将你拖入递归地狱的陷阱并给你一套万无一失的使用法则。一、问题复现只是访问了一个属性程序却疯了场景 1在__getattribute__中访问self.xxx栈溢出classPerson:def__init__(self,name):self.namenamedef__getattribute__(self,item):print(f访问属性:{item})returnself.name# 试图返回 self.name当你创建一个Person(Alice)并尝试访问p.name时会立刻看到RecursionError: maximum recursion depth exceeded为什么因为__getattribute__中的self.name本身又是一次属性访问它会再次触发__getattribute__然后再次执行self.name……无穷无尽直到栈溢出。场景 2想用__getattr__记录访问却陷入递归classProxy:def__init__(self,obj):self._objobjdef__getattr__(self,item):print(f代理访问:{item})returnself._obj# 注意这里返回的是对象本身不是属性这里并没有递归但混淆了__getattr__的触发条件。如果改为def__getattr__(self,item):returngetattr(self._obj,item)这其实是正确的。但如果你在__getattr__中尝试访问self._obj会先触发常规属性查找_obj在实例__dict__中找到所以不会递归。但如果类中没有_obj__getattr__会为_obj调用然后又触发__getattr__……可能在特定条件下递归。场景 3误用__getattr__拦截所有访问却漏掉了某些属性classDynamic:def__getattr__(self,item):ifitemcolor:returnred# 对其他属性没有返回值隐式返回 None当你访问d.color正常但访问d.size却返回None而不是引发AttributeError这违背了 Python 的协议未定义属性应该抛出AttributeError导致后续代码在静默中产生错误。二、底层原理属性查找的完整链条1. 标准的属性查找顺序当你执行obj.attr时Python 会按照以下顺序查找调用__getattribute__无条件。这是所有属性访问的入口无论属性存不存在都会先执行它。在__getattribute__中默认实现会尝试以下步骤在类型type的__mro__中查找数据描述符实现了__set__或__delete__的描述符如 property、slot 描述符等。在实例的__dict__中查找。在类型及其基类的__dict__中查找非数据描述符和普通属性。如果上述步骤都没有找到并且类中定义了__getattr__那么就会调用__getattr__。如果连__getattr__都没有定义则抛出AttributeError。关键区别__getattribute__是每次属性访问都必定调用的入口。__getattr__是只有当常规查找全部失败后才会调用的“最后补救”。2. 为什么__getattribute__这么容易导致递归因为__getattribute__是入口你在它的实现里任何对self.xxx的访问都会再次触发__getattribute__本身形成一个永无止境的循环。要避免递归你必须绕过__getattribute__直接调用父类或基类的原始实现也就是super().__getattribute__(item)。classSafePerson:def__init__(self,name):self.namenamedef__getattribute__(self,item):print(f访问属性:{item})# 正确调用 object.__getattribute__不走自定义逻辑returnsuper().__getattribute__(item)super().__getattribute__(item)会执行标准的属性查找数据描述符、实例字典、类字典而不会触发自定义的__getattribute__从而切断递归。3.__getattr__的安全性相对较高__getattr__只在常规查找失败后才调用所以你在它的内部访问self.other_attr通常不会递归因为other_attr大概率存在于实例字典或类中不会再次触发__getattr__。但如果你在__getattr__中又访问一个不存在的属性就会递归。4. 对应的__setattr__和__delattr__同样的问题与__getattribute__类似__setattr__在每次属性赋值时被调用。如果你在__setattr__中写self.attr value就会无限递归。必须使用super().__setattr__(key, value)或直接操作self.__dict__[key] value。三、常见陷阱与隐蔽的递归场景陷阱 1在__getattribute__中使用self.__dict__可以但也要小心self.__dict__本身也是一个属性访问但它不会触发__getattribute__吗会的如果你在自定义的__getattribute__中访问self.__dict__同样会递归除非你用super().__getattribute__(__dict__)来获取。然而实践中很多人直接写self.__dict__结果递归。正确方式是def__getattribute__(self,item):# 错误d self.__dict__# 正确dsuper().__getattribute__(__dict__)returnd.get(item,default)陷阱 2在__getattribute__中调用其他方法忘记那也会触发classLogger:def__getattribute__(self,item):self.log_access(item)# self.log_access 又会触发 __getattribute__returnsuper().__getattribute__(item)self.log_access又是一次属性访问又会进入__getattribute__形成递归。你需要用super().__getattribute__(log_access)(item)或直接使用函数引用。陷阱 3只重写__getattr__却误以为能拦截所有访问有些开发者想“监控所有属性访问”就重写__getattr__。结果发现obj.name没有被记录因为name存在于实例字典直接走__getattribute__就返回了__getattr__根本没被调用。若想监控所有访问必须重写__getattribute__。陷阱 4在__getattr__中不恰当地返回默认值破坏协议__getattr__应该仅在属性不存在时被调用并且如果它也无法提供属性必须抛出AttributeError。否则像hasattr、getattr等依赖异常的内置函数会得到错误信息类的行为也变得不可预测。classSilent:def__getattr__(self,item):returnNone# 错误应该 raise AttributeError(item)陷阱 5在__setattr__中使用self.key value导致递归classRecursive:def__setattr__(self,key,value):print(fSetting{key})self.keyvalue# 递归应该用super().__setattr__(key, value)或self.__dict__[key] value。四、正确使用模式优雅而安全的属性控制模式 1使用__getattribute__记录日志classLogged:def__getattribute__(self,item):print(fAccessing{item})returnsuper().__getattribute__(item)这能记录所有属性访问包括__dict__等。注意打印print也会触发属性访问所以不要在里面访问其他属性。模式 2使用__getattr__实现惰性属性classLazyExample:def__init__(self):self._expensive_dataNonedef__getattr__(self,item):ifitemdata:ifself._expensive_dataisNone:print(计算数据...)self._expensive_datacompute()returnself._expensive_dataraiseAttributeError(f{type(self).__name__} 没有属性{item})这里只对data特殊处理其他未定义的属性依然抛出AttributeError。模式 3__getattribute__与__getattr__配合实现代理classProxy:def__init__(self,target):self._targettargetdef__getattribute__(self,item):# 注意除了 _target 之外所有属性访问都转给目标对象ifitem_target:returnsuper().__getattribute__(item)targetsuper().__getattribute__(_target)returngetattr(target,item)这里_target必须通过super()获取否则递归。其他属性全转发。模式 4自定义__setattr__进行类型检查classTyped:_types{}def__setattr__(self,key,value):ifkeyinself._types:ifnotisinstance(value,self._types[key]):raiseTypeError(f{key}必须是{self._types[key]})super().__setattr__(key,value)五、调试与检测递归问题的技巧查看 Traceback递归错误的堆栈会显示重复的__getattribute__调用。观察是在哪里进入循环的。使用traceback打印局部变量但递归深时可能崩溃。可以设置sys.setrecursionlimit暂时调高以看清但不推荐用于修复。临时重命名方法在调试时可以将__getattribute__更名为_getattribute然后手动调用object.__getattribute__逐步排查。在__getattribute__中加入 depth 计数器def__getattribute__(self,item):ifnothasattr(self,_depth):super().__setattr__(_depth,0)depthsuper().__getattribute__(_depth)ifdepth10:raiseRuntimeError(递归过深)super().__setattr__(_depth,depth1)try:returnsuper().__getattribute__(item)finally:super().__setattr__(_depth,depth)静态分析工具目前 linter 无法完全检测这类递归但可以通过代码审查确保每个自定义的__getattribute__和__setattr__都使用了super()。单元测试对可能触发递归的属性访问进行专门测试覆盖所有分支。六、最佳实践总结重写__getattribute__时永远使用super().__getattribute__(item)访问任何自身的属性包括__dict__。如果只是想处理“缺失属性”优先使用__getattr__它更安全且意图清晰。在__getattr__中无法处理时必须raise AttributeError。重写__setattr__和__delattr__时同样使用super()进行实际存储操作。避免在__getattribute__内部调用其他需要属性访问的方法若有需要通过super().__getattribute__获取方法对象再调用。为复杂的属性控制逻辑编写明确的文档说明哪些属性是特殊的哪些是转发的。考虑使用框架提供的描述符或__slots__来替代自定义的__getattribute__以减少出错机会。在类的__init__中为所有常规属性赋值时注意不要触发自定义的__setattr__如果它已定义确保初始化正常完成。七、结语__getattr__和__getattribute__就像是 Python 属性世界的两道防线__getattribute__是忠于职守的前线哨兵每次访问都必须经过它的盘查而__getattr__则是后方的情报官只有在前线找不到目标时才会介入。一旦你让哨兵自己去问另一个哨兵“这个属性在哪”就会陷入无穷的自我汇报中。掌握了super().__getattribute__这把特权通道你才能让哨兵在不干扰自己的情况下完成检查。从此无论你构建多么动态的属性系统都不会再被递归的漩涡吞噬而是能让属性访问如臂使指干净利落。
你的属性访问为何陷入无限递归?——Python __getattr__ 与 __getattribute__ 的致命区别与掌控秘籍
你的属性访问为何陷入无限递归——Python__getattr__与__getattribute__的致命区别与掌控秘籍在 Python 的面向对象编程中__getattr__和__getattribute__是两个无比强大的魔方法它们让你能完全控制属性的访问行为。你可以用它们实现“懒加载”、“属性代理”、“虚拟属性”、“日志记录”等高级特性。然而许多开发者在这里跌进一个万劫不复的深渊无限递归。程序在瞬间耗尽调用栈抛出RecursionError而你甚至找不到到底哪里引发了递归。更隐蔽的是有时候递归发生在特定条件才触发测试时一切正常上线后突然崩溃。这一切都源于对这两个方法的根本性误解它们虽然名字相似但触发时机、职责边界和必须遵守的安全规则截然不同。今天我们就来彻底解剖这对“属性访问双雄”的底层机制揭开那些将你拖入递归地狱的陷阱并给你一套万无一失的使用法则。一、问题复现只是访问了一个属性程序却疯了场景 1在__getattribute__中访问self.xxx栈溢出classPerson:def__init__(self,name):self.namenamedef__getattribute__(self,item):print(f访问属性:{item})returnself.name# 试图返回 self.name当你创建一个Person(Alice)并尝试访问p.name时会立刻看到RecursionError: maximum recursion depth exceeded为什么因为__getattribute__中的self.name本身又是一次属性访问它会再次触发__getattribute__然后再次执行self.name……无穷无尽直到栈溢出。场景 2想用__getattr__记录访问却陷入递归classProxy:def__init__(self,obj):self._objobjdef__getattr__(self,item):print(f代理访问:{item})returnself._obj# 注意这里返回的是对象本身不是属性这里并没有递归但混淆了__getattr__的触发条件。如果改为def__getattr__(self,item):returngetattr(self._obj,item)这其实是正确的。但如果你在__getattr__中尝试访问self._obj会先触发常规属性查找_obj在实例__dict__中找到所以不会递归。但如果类中没有_obj__getattr__会为_obj调用然后又触发__getattr__……可能在特定条件下递归。场景 3误用__getattr__拦截所有访问却漏掉了某些属性classDynamic:def__getattr__(self,item):ifitemcolor:returnred# 对其他属性没有返回值隐式返回 None当你访问d.color正常但访问d.size却返回None而不是引发AttributeError这违背了 Python 的协议未定义属性应该抛出AttributeError导致后续代码在静默中产生错误。二、底层原理属性查找的完整链条1. 标准的属性查找顺序当你执行obj.attr时Python 会按照以下顺序查找调用__getattribute__无条件。这是所有属性访问的入口无论属性存不存在都会先执行它。在__getattribute__中默认实现会尝试以下步骤在类型type的__mro__中查找数据描述符实现了__set__或__delete__的描述符如 property、slot 描述符等。在实例的__dict__中查找。在类型及其基类的__dict__中查找非数据描述符和普通属性。如果上述步骤都没有找到并且类中定义了__getattr__那么就会调用__getattr__。如果连__getattr__都没有定义则抛出AttributeError。关键区别__getattribute__是每次属性访问都必定调用的入口。__getattr__是只有当常规查找全部失败后才会调用的“最后补救”。2. 为什么__getattribute__这么容易导致递归因为__getattribute__是入口你在它的实现里任何对self.xxx的访问都会再次触发__getattribute__本身形成一个永无止境的循环。要避免递归你必须绕过__getattribute__直接调用父类或基类的原始实现也就是super().__getattribute__(item)。classSafePerson:def__init__(self,name):self.namenamedef__getattribute__(self,item):print(f访问属性:{item})# 正确调用 object.__getattribute__不走自定义逻辑returnsuper().__getattribute__(item)super().__getattribute__(item)会执行标准的属性查找数据描述符、实例字典、类字典而不会触发自定义的__getattribute__从而切断递归。3.__getattr__的安全性相对较高__getattr__只在常规查找失败后才调用所以你在它的内部访问self.other_attr通常不会递归因为other_attr大概率存在于实例字典或类中不会再次触发__getattr__。但如果你在__getattr__中又访问一个不存在的属性就会递归。4. 对应的__setattr__和__delattr__同样的问题与__getattribute__类似__setattr__在每次属性赋值时被调用。如果你在__setattr__中写self.attr value就会无限递归。必须使用super().__setattr__(key, value)或直接操作self.__dict__[key] value。三、常见陷阱与隐蔽的递归场景陷阱 1在__getattribute__中使用self.__dict__可以但也要小心self.__dict__本身也是一个属性访问但它不会触发__getattribute__吗会的如果你在自定义的__getattribute__中访问self.__dict__同样会递归除非你用super().__getattribute__(__dict__)来获取。然而实践中很多人直接写self.__dict__结果递归。正确方式是def__getattribute__(self,item):# 错误d self.__dict__# 正确dsuper().__getattribute__(__dict__)returnd.get(item,default)陷阱 2在__getattribute__中调用其他方法忘记那也会触发classLogger:def__getattribute__(self,item):self.log_access(item)# self.log_access 又会触发 __getattribute__returnsuper().__getattribute__(item)self.log_access又是一次属性访问又会进入__getattribute__形成递归。你需要用super().__getattribute__(log_access)(item)或直接使用函数引用。陷阱 3只重写__getattr__却误以为能拦截所有访问有些开发者想“监控所有属性访问”就重写__getattr__。结果发现obj.name没有被记录因为name存在于实例字典直接走__getattribute__就返回了__getattr__根本没被调用。若想监控所有访问必须重写__getattribute__。陷阱 4在__getattr__中不恰当地返回默认值破坏协议__getattr__应该仅在属性不存在时被调用并且如果它也无法提供属性必须抛出AttributeError。否则像hasattr、getattr等依赖异常的内置函数会得到错误信息类的行为也变得不可预测。classSilent:def__getattr__(self,item):returnNone# 错误应该 raise AttributeError(item)陷阱 5在__setattr__中使用self.key value导致递归classRecursive:def__setattr__(self,key,value):print(fSetting{key})self.keyvalue# 递归应该用super().__setattr__(key, value)或self.__dict__[key] value。四、正确使用模式优雅而安全的属性控制模式 1使用__getattribute__记录日志classLogged:def__getattribute__(self,item):print(fAccessing{item})returnsuper().__getattribute__(item)这能记录所有属性访问包括__dict__等。注意打印print也会触发属性访问所以不要在里面访问其他属性。模式 2使用__getattr__实现惰性属性classLazyExample:def__init__(self):self._expensive_dataNonedef__getattr__(self,item):ifitemdata:ifself._expensive_dataisNone:print(计算数据...)self._expensive_datacompute()returnself._expensive_dataraiseAttributeError(f{type(self).__name__} 没有属性{item})这里只对data特殊处理其他未定义的属性依然抛出AttributeError。模式 3__getattribute__与__getattr__配合实现代理classProxy:def__init__(self,target):self._targettargetdef__getattribute__(self,item):# 注意除了 _target 之外所有属性访问都转给目标对象ifitem_target:returnsuper().__getattribute__(item)targetsuper().__getattribute__(_target)returngetattr(target,item)这里_target必须通过super()获取否则递归。其他属性全转发。模式 4自定义__setattr__进行类型检查classTyped:_types{}def__setattr__(self,key,value):ifkeyinself._types:ifnotisinstance(value,self._types[key]):raiseTypeError(f{key}必须是{self._types[key]})super().__setattr__(key,value)五、调试与检测递归问题的技巧查看 Traceback递归错误的堆栈会显示重复的__getattribute__调用。观察是在哪里进入循环的。使用traceback打印局部变量但递归深时可能崩溃。可以设置sys.setrecursionlimit暂时调高以看清但不推荐用于修复。临时重命名方法在调试时可以将__getattribute__更名为_getattribute然后手动调用object.__getattribute__逐步排查。在__getattribute__中加入 depth 计数器def__getattribute__(self,item):ifnothasattr(self,_depth):super().__setattr__(_depth,0)depthsuper().__getattribute__(_depth)ifdepth10:raiseRuntimeError(递归过深)super().__setattr__(_depth,depth1)try:returnsuper().__getattribute__(item)finally:super().__setattr__(_depth,depth)静态分析工具目前 linter 无法完全检测这类递归但可以通过代码审查确保每个自定义的__getattribute__和__setattr__都使用了super()。单元测试对可能触发递归的属性访问进行专门测试覆盖所有分支。六、最佳实践总结重写__getattribute__时永远使用super().__getattribute__(item)访问任何自身的属性包括__dict__。如果只是想处理“缺失属性”优先使用__getattr__它更安全且意图清晰。在__getattr__中无法处理时必须raise AttributeError。重写__setattr__和__delattr__时同样使用super()进行实际存储操作。避免在__getattribute__内部调用其他需要属性访问的方法若有需要通过super().__getattribute__获取方法对象再调用。为复杂的属性控制逻辑编写明确的文档说明哪些属性是特殊的哪些是转发的。考虑使用框架提供的描述符或__slots__来替代自定义的__getattribute__以减少出错机会。在类的__init__中为所有常规属性赋值时注意不要触发自定义的__setattr__如果它已定义确保初始化正常完成。七、结语__getattr__和__getattribute__就像是 Python 属性世界的两道防线__getattribute__是忠于职守的前线哨兵每次访问都必须经过它的盘查而__getattr__则是后方的情报官只有在前线找不到目标时才会介入。一旦你让哨兵自己去问另一个哨兵“这个属性在哪”就会陷入无穷的自我汇报中。掌握了super().__getattribute__这把特权通道你才能让哨兵在不干扰自己的情况下完成检查。从此无论你构建多么动态的属性系统都不会再被递归的漩涡吞噬而是能让属性访问如臂使指干净利落。