Python属性查找的MRO路径是「实例 → 类 → 父类(按MRO顺序)」,即先查实例__dict__,再按__class__.__mro__元组从左到右依次查找类及其父类中的属性。

Python 中属性查找的 MRO 路径是什么
Python 查找属性时,不是先看类再看实例,也不是先看实例再看类,而是严格遵循「实例 → 类 → 父类(按 MRO 顺序)」这条链。这个顺序决定了 obj.attr 最终取到的是哪个值,和你是否用 @property、是否重写了 __getattribute__ 无关——那是更底层的拦截机制,普通情况不介入。
你可以用 obj.__class__.__mro__ 看到实际的继承链,比如 class B(A): pass,那么 B().__class__.__mro__ 返回的就是 (。查找就按这个元组从左到右依次尝试获取 attr。
类属性被实例属性遮蔽的典型现象
只要实例上存在同名属性(哪怕只是赋值过一次),后续对该名称的读取就永远不会再落到类属性上——不是“覆盖”,是“遮蔽”。这是最常让人困惑的点。
-
class C: x = 10,然后c = C(),此时c.x是10(来自类) - 执行
c.x = 20后,c.x变成20(现在来自实例),而C.x仍是10 - 再执行
del c.x,c.x又变回10(重新命中类属性) - 如果类中定义了
__slots__ = ['x'],但没在实例中赋值过x,那c.x仍会查类属性;一旦赋值,就进入 slots 插槽,同样遮蔽类属性
修改类属性影响所有未遮蔽的实例
类属性本质是类对象的一个普通属性,修改它只改变那个内存地址上的值。所有尚未遮蔽该属性的实例,在下次读取时都会看到新值。
立即学习“Python免费学习笔记(深入)”;
-
class D: counter = 0,d1, d2 = D(), D(),此时d1.counter和d2.counter都是0 -
D.counter = 100,d1.counter和d2.counter立即变成100 - 但如果之前执行过
d1.counter = 5,那d1.counter就不再响应D.counter的变化 - 对可变类属性(如
list、dict)直接原地修改(D.items.append(1)),即使有实例遮蔽,也不会影响已遮蔽的那些——因为它们指向不同对象
想绕过实例遮蔽直接读类属性怎么办
不能靠 obj.__class__.attr,因为如果父类也有同名属性,它不一定就是你想要的那个;正确方式是明确走 MRO 或直接访问类本身。
- 用
type(obj).__dict__.get('attr')只查当前类的定义(不含继承),适合确认“这个类自己有没有定义” - 用
getattr(type(obj), 'attr', None)才真正模拟属性查找逻辑(含 MRO),等价于去掉实例层的查找 - 如果知道具体类名,比如
C.x,那就最直白;但要注意这跟obj.x行为可能完全不同 - 别用
obj.__dict__['attr']去“强制读实例”,因为实例可能根本没这个 key(比如用了__slots__或属性被@property拦截)
真正容易出问题的不是顺序本身,而是默认以为“类属性改了,所有实例都同步”,结果发现有些实例早被遮蔽了;或者误把 obj.attr = ... 当成“设置类属性”,其实只是给实例加字段。查 MRO、看 __dict__、少用可变类属性,这三个动作比背顺序管用得多。










