Python getattr与getattribute的调用优先级差异
理解 __getattr__ 和 __getattribute__ 的调用顺序是掌握 Python 属性访问机制的关键。一个无条件触发,另一个是后备方案,搞混会导致无限递归或意外行为。
1. 核心差异对比
首先,通过一个表格快速把握两者本质。
| 特性 | __getattribute__ |
__getattr__ |
|---|---|---|
| 触发时机 | 无条件触发。每次通过实例访问属性(如 obj.name)时,首先调用此方法。 |
条件触发。仅在通过常规方式(如查字典)找不到属性时,作为最后手段被调用。 |
| 作用 | 拦截并控制所有属性访问请求。 | 处理未定义属性的访问,常用于实现动态属性或提供默认值。 |
| 风险 | 高。若在其中访问 self.xxx,极易引发无限递归。 |
低。作为安全的后备机制。 |
| 典型用途 | 实现代理、属性访问日志、严格访问控制。 | 实现属性默认值、动态属性生成(如从数据库加载)。 |
2. 调用优先级顺序详解
当执行 obj.x 时,Python 的内部查找顺序如下:
- 调用
type(obj).__getattribute__(obj, 'x')。 - 在
__getattribute__内部,通常会按顺序检查:- 实例的
__dict__(对象的属性字典)。 - 类的
__dict__及其所有基类的__dict__(通过描述符协议)。 - 如果以上步骤都找不到属性
x,则最终调用type(obj).__getattr__(obj, 'x')。
- 实例的
简单记忆:__getattribute__ 是必经的第一道大门,__getattr__ 是仓库没找到东西后的失物招领处。
3. 动手验证:一个关键示例
定义一个类,同时重写这两个方法,并加入 print 语句来追踪调用顺序。
class AttributeDemo:
def __init__(self):
self.existing_attr = "我存在于实例字典中"
def __getattribute__(self, name):
# 必须调用父类(object)的__getattribute__来执行实际查找
# 否则所有属性访问都会失败,并引发AttributeError
print(f"__getattribute__ 被调用,查找属性: '{name}'")
# 注意:这里访问 self.existing_attr 会再次触发__getattribute__(递归风险)
# 安全做法是直接使用super()
try:
return super().__getattribute__(name)
except AttributeError:
# 如果常规查找失败(实例和类字典都没有),我们选择让__getattr__处理
# 此时,__getattribute__方法本身不应引发这个异常
# 所以我们raise它,由Python的协议转去调用__getattr__
raise
def __getattr__(self, name):
# 只有当__getattribute__中的常规查找失败后才会执行
print(f"__getattr__ 被调用,属性 '{name}' 未找到")
return f"为 '{name}' 生成的默认值"
创建实例并访问不同属性:
demo = AttributeDemo()
# 情况1:访问一个存在的属性
print("--- 访问存在的属性 ---")
print(f"结果: {demo.existing_attr}")
# 情况2:访问一个不存在的属性
print("\n--- 访问不存在的属性 ---")
print(f"结果: {demo.nonexistent_attr}")
输出结果清晰展示了调用优先级:
--- 访问存在的属性 ---
__getattribute__ 被调用,查找属性: 'existing_attr'
结果: 我存在于实例字典中
--- 访问不存在的属性 ---
__getattribute__ 被调用,查找属性: 'nonexistent_attr'
__getattr__ 被调用,属性 'nonexistent_attr' 未找到
结果: 为 'nonexistent_attr' 生成的默认值
4. 常见陷阱与最佳实践
陷阱:在 __getattribute__ 中访问 self.xxx 导致无限递归。
在 __getattribute__ 内部,任何对 self.其他属性 的访问都会再次触发 __getattribute__,形成无限循环。解决方案是使用 super().__getattribute__(xxx) 或直接操作实例的 __dict__。
# 错误示范(会递归)
class Bad:
def __getattribute__(self, name):
if name == 'y':
return self.y # 灾难!再次触发__getattribute__
return super().__getattribute__(name)
# 正确做法
class Good:
def __getattribute__(self, name):
if name == 'y':
# 方法1:通过父类方法安全获取(如果y存在于字典中)
# return super().__getattribute__('y')
# 方法2:直接从实例字典获取
return self.__dict__.get('y', None)
return super().__getattribute__(name)
最佳实践:
- 谨慎重写
__getattribute__,通常只在需要绝对控制所有属性访问时(如代理模式、调试)才使用。 - 在
__getattribute__中,务必调用super().__getattribute__(name)来执行标准查找,除非你想改变基本的属性访问语义。 - 将
__getattr__作为安全、便捷的后备机制,用于处理缺失属性,代码更清晰、风险更低。 - 当你需要拦截对特定属性的访问(如计算属性、验证)时,优先考虑使用
@property描述符,它比无差别拦截的__getattribute__更精准、更安全。
5. 结论:明确的优先级链
属性访问的完整优先级链为:__getattribute__ -> 实例字典 -> 类字典(含描述符) -> __getattr__。
__getattribute__ 是守门员,控制着通往所有属性的大门。__getattr__ 是应急方案,只在前门守卫说“这里没有”时才会被启用。明确这个顺序,你就能避免递归深渊,合理利用这两个强大的钩子来设计灵活的 Python 类。

暂无评论,快来抢沙发吧!