Python:属性访问机制
在 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 对象模型一致性与动态能力的基础。

“点赞有美意,赞赏是鼓励”
更多推荐


所有评论(0)