在 Python 中,属性访问(Attribute Access)并不是“从对象中取一个字段”的简单操作,而是一项由语言规范明确规定的运行期解析与调度过程。

无论访问的是实例属性、类属性、方法,还是由描述符控制的属性,甚至是否触发方法绑定,解释器都遵循同一条统一的属性访问主流程。

理解这一流程,是理解 Python 对象模型一致性、动态能力与可组合机制的关键。

一、属性访问是一项运行期解析请求

当我们写下:

obj.attr

从对象模型的角度看,这并不意味着“从 obj 的内部结构中直接读取 attr 的值”,而是向解释器发出一次解析请求:在当前运行期上下文中,obj.attr 应当如何表现?

这一区别极其重要,因为在 Python 中:

• 属性可能不存在

• 属性可能是方法

• 属性可能受描述符控制

• 属性的返回结果可能是运行期动态生成的

因此,Python 将属性访问定义为一项运行期行为解析过程,而非静态字段读取。

二、所有属性访问的统一入口:__getattribute__

只要发生属性访问:

obj.attr

在语义层面,都等价于:

type(obj).__getattribute__(obj, "attr")

这一点不因对象类型而改变。无论访问的是:

• 实例对象

• 类对象

• 模块对象

属性访问总是通过 type(obj).__getattribute__ 进入,其默认实现由 object 或 type 等基础类型提供。

三、属性访问流程的总体结构

在不考虑用户重写 __getattribute__ 的情况下,一次完整的属性访问流程可以拆解为以下阶段:

1、确立访问者对象(实例 / 类 / 模块)

2、进入 __getattribute__ 统一入口

3、按既定规则执行属性查找

4、在查找过程中识别并处理描述符

5、在返回阶段处理方法绑定等派生语义

6、若解析失败,进入异常或补救路径

下面将按这一顺序逐步展开。

四、查找阶段:属性从哪里被查找

1、查找并不是“实例先、类后”的简单规则

“先查实例,再查类”是一种教学简化说法,但在机制层面并不精确。

更准确的表述是:属性查找遵循一套由对象模型定义的固定顺序,而非由实例或类自行决定。

在默认实现中,执行 obj.attr 时,核心查找顺序为:

1、类 __dict__ 中的数据描述符

2、实例 obj.__dict__

3、类 __dict__ 中的非数据描述符

4、类 __dict__ 中的普通属性

5、若仍未找到,进入 __getattr__

说明:若类未找到属性名,则按照 MRO 线性顺序继续向基类查找。

这一顺序并非经验总结,而是描述符协议与实例字典优先级共同作用的结果。

2、实例与类并不“执行查找”

一个常见误解是:“实例负责查实例属性,类负责查类属性”。

从对象模型角度看,这是不成立的。

实例与类并不执行查找逻辑,它们只是:

• 提供命名空间(__dict__)

• 构成查找路径中的节点

真正执行查找、判断优先级、决定是否介入描述符的,是 __getattribute__ 所代表的统一机制。

五、描述符在属性解析中的介入时机

在属性查找过程中,一旦在类命名空间中定位到某个属性对象,解释器会检查:该对象是否实现了描述符协议方法(__get__、__set__、__delete__),若是,则描述符协议介入属性解析流程。

1、数据描述符的优先级

若对象实现了 __set__ 或 __delete__,则它被视为数据描述符。

其特点是:

• 在查找顺序中优先于实例 __dict__

• 实例无法通过同名属性绕过其控制

这正是 property 等机制能够可靠约束实例状态的根本原因。

2、非数据描述符的优先级

若对象仅实现了 __get__,则它是非数据描述符。

其优先级:

• 低于实例 __dict__

• 高于普通类属性

这也是方法可以被实例属性遮蔽的直接原因。

六、返回阶段:方法绑定不是“查找到的结果”

属性查找结束,并不意味着流程终止。返回阶段本身就是属性访问机制的一部分。

考虑以下代码:

class A:    def foo(self):        return "foo"
a = A()m = a.foo

这里发生的并不是简单的“找到了 foo 方法”,而是:

1、在 A.__dict__ 中定位到函数对象 foo

2、识别其为非数据描述符

3、因访问者是实例 a

4、调用 foo.__get__(a, A),构造并返回一个绑定方法对象

因此可以严格地说,方法绑定不是函数的固有属性,而是属性访问在返回阶段生成的运行期结果。

七、类访问与实例访问为何表现不同

对比以下两种访问:

A.fooa.foo

二者的差异不在于 foo 是否为方法,而在于:

• 访问者对象不同

• 描述符在 __get__ 中接收到的 instance 参数不同

foo.__get__(None, A)   # 类访问foo.__get__(a, A)      # 实例访问

是否生成绑定方法对象,完全由这一参数决定。

八、属性访问失败与 __getattr__

若在完整解析流程中未能得到结果,解释器将检查对象是否定义了 __getattr__:

• 若存在,调用该方法作为补救路径

• 若仍失败,抛出 AttributeError

需要明确的是,__getattr__ 并不是属性查找流程的一部分,而是查找失败后的兜底处理。

它不会改变正常的查找顺序,只在“完全解析失败”时才介入。

📘 小结

Python 的属性访问并非字段读取,而是一项由统一入口调度、在命名空间之间协调、并在必要时由描述符协议介入的运行期解析机制。方法绑定、动态属性与多态行为,皆是在该机制中自然生成的结果。

理解属性访问机制,是把握 Python 对象模型一致性与动态能力的基础。

图片

“点赞有美意,赞赏是鼓励”

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐