用 getattr 实现属性兜底可行,但它仅在常规查找失败(如__dict__、父类属性、描述符、__getattribute__抛AttributeError)后触发,不是通用拦截器。

直接说结论:用 __getattr__ 实现属性兜底是可行的,但它只在常规查找失败后触发——也就是说,__dict__、父类属性、描述符、__getattribute__ 显式抛出 AttributeError 之后才会调用它。别把它当成“所有属性访问的拦截器”,否则会踩坑。
为什么 __getattr__ 没被调用?常见触发失败场景
很多人写了 __getattr__ 却发现根本没执行,原因通常是:
- 属性其实在实例
__dict__中已存在(比如你赋过值:obj.x = 1),__getattr__完全不介入 - 属性定义在类上(非描述符),例如
class A: y = 2,访问obj.y直接命中,不走__getattr__ - 用了
__getattribute__但没在异常分支里 re-raiseAttributeError,导致流程卡住,__getattr__被跳过 - 拼写错误:写成了
__getattr(少下划线)或__getattribute__(混淆用途)
__getattr__ 的典型用法:代理、默认值、懒加载
它适合做“查无此属性时的友好 fallback”,不是通用拦截。几个真实可用的模式:
- 返回默认值:
return self._defaults.get(name, None) - 委托给内部对象:
return getattr(self._data, name)(注意加try/except AttributeError避免无限递归) - 动态生成属性(如字段映射):
if name.startswith('is_'): return getattr(self._raw, name[3:], False) - 抛出自定义错误,比原生
AttributeError更明确:raise AttributeError(f"{type(self).__name__} has no field '{name}'")
示例:
class Config:
def __init__(self):
self._values = {'host': 'localhost', 'port': 8000}
<pre class="brush:php;toolbar:false;">def __getattr__(self, name):
if name in self._values:
return self._values[name]
raise AttributeError(f"Config has no attribute '{name}'")cfg = Config() print(cfg.host) # → 'localhost' print(cfg.timeout) # → AttributeError: Config has no attribute 'timeout'
和 __getattribute__ 的关键区别与协作方式
__getattribute__ 是“全量拦截”,每次属性访问都过它;__getattr__ 是“查无此人后的补救”。二者常配合使用:
- 想记录所有访问?重写
__getattribute__,但必须显式调用super().__getattribute__(name)否则递归爆炸 - 只想处理缺失属性?只用
__getattr__,更安全、更轻量 - 混合策略:在
__getattribute__中预处理特定前缀(如_cache_),其余交给默认逻辑,最终由__getattr__收尾
注意:如果重写了 __getattribute__ 却忘了抛 AttributeError,__getattr__ 就永远没机会运行。
最容易被忽略的一点:__getattr__ 不影响 hasattr() 和 getattr(obj, name, default) 的语义——它们内部就是靠捕获 AttributeError 来判断的,所以你的 __getattr__ 必须严格只对真正不存在的属性抛错,否则 hasattr(obj, 'x') 可能返回意外的 False。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











