Python 类属性与实例属性的查找顺序

12次阅读

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

Python 类属性与实例属性的查找顺序

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.x10(来自类)
  • 执行 c.x = 20 后,c.x 变成 20(现在来自实例),而 C.x 仍是 10
  • 再执行 del c.xc.x 又变回 10(重新命中类属性)
  • 如果类中定义了 __slots__ = ['x'],但没在实例中赋值过 x,那 c.x 仍会查类属性;一旦赋值,就进入 slots 插槽,同样遮蔽类属性

修改类属性影响所有未遮蔽的实例

类属性本质是类对象的一个普通属性,修改它只改变那个内存地址上的值。所有尚未遮蔽该属性的实例,在下次读取时都会看到新值。

立即学习Python免费学习笔记(深入)”;

  • class D: counter = 0d1, d2 = D(), D(),此时 d1.counterd2.counter 都是 0
  • D.counter = 100d1.counterd2.counter 立即变成 100
  • 但如果之前执行过 d1.counter = 5,那 d1.counter 就不再响应 D.counter 的变化
  • 对可变类属性(如 listdict)直接原地修改(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__、少用可变类属性,这三个动作比背顺序管用得多。

text=ZqhQzanResources