一段"配置读取"的代码用 __getattr__ 兜底:任何未定义的属性都从字典里取。上线后一个拼写错误的属性名(conf.timeuot)没有报错,而是静默返回了 None——因为 __getattr__ 被调用了,而它从字典里没找到就返回 None。
Python 的属性访问有三层钩子,而它们的"触发条件"完全不同:__getattribute__ 每次都调、__getattr__ 只在找不到时调、描述符在类层面接管访问。这个区分的实际意义是"出错方式":用 property 写的逻辑,出错会抛异常;用 __getattr__ 兜底的逻辑,出错会静默返回默认值——而后者在生产上更难发现。
而"用哪个"决定了"出错时是报错还是静默"。
1. 三层的触发条件
| 钩子 | 触发时机 |
|---|---|
__getattribute__ |
每次属性访问 |
__getattr__ |
常规查找失败后 |
描述符(__get__) |
类属性被访问时 |
property |
本质是描述符的一种 |
第二项是"兜底",而兜底的问题就是"它掩盖了拼写错误"。
第三项是"属性代理"的底层机制:property 就是一个实现了 __get__ 的类第三项的"投资回报"很高:理解描述符之后,property、classmethod、staticmethod、乃至 ORM 的字段声明都能一句话解释——它们都是"类属性上的描述符"。
,所以"理解描述符"能解释 property 的一切行为。
2. __getattr__ 的三种风险
| 风险 | 表现 |
|---|---|
| 吞掉拼写错误 | 返回 None 而不是 AttributeError |
干扰 hasattr |
hasattr 永远为 True |
与 __getattribute__ 混用 |
递归/性能问题 |
第一项的"变体"是"兜底返回空字典/空列表":于是"取配置失败"表现为"配置项为空",而下游"空也不报错"——错误被推迟到很远的地方才暴露。
第一项最实际:conf.timeuot 静默返回 None,于是"配置没生效"而不是"报错"。
第二项让"存在性判断"失效:hasattr(obj, 'anything') 都返回 True第二项的连带影响是"getattr(obj, 'x', default) 的默认值永远用不上",于是"没这个属性就用默认"这条逻辑失效——而它看起来完全正常。
,于是"特性检测"完全不可用。
// 反例:无约束的 __getattr__ 兜底
class Conf:
def __getattr__(self, name):
return self._data.get(name) # 拼错 → None
// 正解:兜底但抛错(区分"有值"与"没这个键")
class Conf:
def __getattr__(self, name):
if name in self._data:
return self._data[name]
raise AttributeError(f"unknown config: {name}") # 拼错就报错
3. property 的四个用途
| 用途 | 说明 |
|---|---|
| 计算属性 | 从已有字段派生 |
| 校验赋值 | setter 里检查 |
| 懒加载 | 首次访问时计算 |
| 只读封装 | 只定义 getter |
第四项的"陷阱"在于"它连内部赋值也禁止":self.name = x 在 __init__ 里也会抛,于是"只读属性"必须在 __init__ 里写 self.__dict__['name'] = x(或用 object.__setattr__)。
第四项有个"陷阱":只定义 getter 时,赋值会抛 AttributeError——看起来"实现了只读",但它与"不该有 setter"的语义不完全一致(内部也不能赋值)。
第二项在"字段规范化"时有用:self._name = value.strip() 放在 setter 里,于是"赋值就规范"。
// 四种用途的写法(懒加载最容易写错)
class Report:
@property
def rows(self):
if self._rows is None: # 懒加载:首次访问才查库
self._rows = self._load()
return self._rows
// 注意:懒加载 + 多线程时需要加锁,否则可能重复加载
4. 描述符与 property 的关系
| 写法 | 等价于 |
|---|---|
@property |
一个 __get__ 描述符 |
@x.setter |
给同一描述符加 __set__ |
| 自定义描述符类 | 可复用的属性行为 |
第三项在"多字段同规则"时最有用:例如十个字段都要"转小写",用一个描述符类比写十个 property 更省。第三项的"复用"是它相对 property 的唯一优势,但优势很大:一个描述符类可以服务几十个字段与多个类,而 property 必须每个都写一遍。
// 可复用描述符:多字段同一规则
class Lower:
def __set_name__(self, owner, name): self.name = name
def __get__(self, obj, objtype=None):
return None if obj is None else obj.__dict__.get(self.name)
def __set__(self, obj, value):
obj.__dict__[self.name] = value.lower() # 赋值即转小写
class User:
email = Lower() # 一个类,多处复用
nickname = Lower()
"属性协议"还有一个常被忽略的入口:__set_name__。它在类创建时被调用,于是描述符可以"知道自己叫哪个名字"——这让"存到 __dict__ 的哪个键"自动化,避免手写 _name。
// 用 __set_name__ 自动取名(无需手写 _name)
class Lower:
def __set_name__(self, owner, name):
self.name = name # 自动知道叫 "email"
def __get__(self, obj, objtype=None):
if obj is None: return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
obj.__dict__[self.name] = value.lower()
class User:
email = Lower() # __set_name__ 在这里被调用,self.name = "email"
5. 什么时候用哪个
| 需求 | 选择 |
|---|---|
| 一个类的少量计算属性 | property |
| 多个类/字段同一规则 | 自定义描述符 |
| 动态代理(未知字段) | __getattr__(要抛错) |
| 全局拦截所有访问 | __getattribute__(慎用) |
第四项的"性能"影响也不小:Python 的普通属性访问已经绕了几层,__getattribute__ 再包一层会让"高频字段访问"明显变慢——而在热点路径上,这个开销是可测的。
第四项最危险:它拦截"每一次"访问,包括 self._x 内部访问,于是内部字段也可能被拦截——而最坏的情况是"拦截逻辑里访问了自身属性"造成无限递归。
"属性访问"的问题可以用"三行自测"在开发期暴露:拼错的属性、hasattr 的真假、.get 与属性的差异。
// 三行自测(写在测试里,防"静默兜底")
assert not hasattr(Conf({}), "definitely_missing") # 不该 True
with pytest.raises(AttributeError): Conf({}).timeuot # 拼错要报错
assert Conf({"timeout": 3}).timeout == 3 # 正常路径
// 若第 1、2 项失败 → 你的 __getattr__ 兜得太宽
6. 排错:五种"属性访问问题"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 拼错属性不报错 | __getattr__ 无约束 |
抛 AttributeError |
hasattr 总是 True |
同上 | 同上 |
property 里递归 |
getter 访问了自身 | 用 self.__dict__ |
| 懒加载重复计算 | 无锁/无标记 | 加标记 |
| 赋值没生效 | 只定义了 getter | 补 setter |
第三项很常见:@property def name(self): return self.name 会无限递归第三项的修法是"绕开属性协议":在 getter 里用 self.__dict__["name"] 或 object.__getattribute__(self, "_name")——而"用不同名字存"(_name)是更常见的做法。
——因为 self.name 触发的还是这个 property。
最后一个"性能"边界值得量化:property 的访问比公共属性慢约 2–3 倍,__getattribute__ 再慢一档。而在"每秒几十万次字段访问"的热点路径上,这个差别会体现在 CPU 上。
// 粗略量级(Python 3.11,纳秒级)
公共属性: ~20ns
property: ~60ns(含额外调用)
__getattribute__:~120ns(拦截所有访问)
// 判据:热点路径上"是否每秒访问百万次以上"
7. 边界:什么情况不必用这些钩子
| 场景 | 为什么 | 做法 |
|---|---|---|
| 简单数据类 | 直接公共属性 | 不写 property |
| 无校验需求 | 无需 setter | 不写 |
| 内部小工具 | 出错可见 | 不兜底 |
已用 dataclass |
有现成机制 | 跟随 |
判断口径一句话:这个属性"访问时需不需要做事"(计算、校验、懒加载)。需要,用 property;不需要,公共属性更简单——而"为了封装而封装"会让代码更长,却不更安全。
8. 自测
- 三层钩子的触发条件分别是什么?为什么
__getattr__会"吞掉拼写错误"? - 为什么"
hasattr永远为 True"会让特性检测失效? property的四个用途里,哪一个与多线程有关?要注意什么?- 自定义描述符在什么场景比
property更省?"赋值即处理"怎么实现? - 为什么
__getattribute__最危险?"无限递归"是怎么发生的? - 判断"是否需要 property"的那个问题是什么?