Python属性查找的MRO路径是「实例 → 类 → 父类(按MRO顺序)」,即先查实例__dict__,再按__class__.__mro__元组从左到右依次查找类及其父类中的属性。
Python 查找属性时,不是先看类再看实例,也不是先看实例再看类,而是严格遵循「实例 → 类 → 父类(按 MRO 顺序)」这条链。这个顺序决定了 obj.attr 最终取到的是哪个值,和你是否用 @property、是否重写了 __getattribute__ 无关——那是更底层的拦截机制,普通情况不介入。
你可以用 obj.__class__.__mro__ 看到实际的继承链,比如 class B(A): ,那么 
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 插槽,同样遮蔽类属性类属性本质是类对象的一个普通属性,修改它只改变那个内存地址上的值。所有尚未遮蔽该属性的实例,在下次读取时都会看到新值。
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__、少用可变类属性,这三个动作比背顺序管用得多。