在 Python 的对象模型中,描述符对象(Descriptor Objects)是支撑语言动态特性的核心机制之一。从最基础的属性访问,到复杂的元编程框架(如 Django ORM、SQLAlchemy、Pydantic 的字段系统),描述符始终处于幕后,却决定着属性系统的最终行为。
如果说 __dict__ 体系提供了属性数据的静态存储结构,那么描述符对象就是介入这一结构之上的动态访问控制层。
需要强调的是,描述符不是特殊语法或内建魔法,而是完全遵循 Python 对象模型的普通对象。
一、描述符对象的概念
(1)描述符是对象
在 Python 中,一切皆对象。描述符也不例外:
• 它是某个类的实例
• 拥有自身的类型、属性与方法
• 可以被赋值、传递并存储于 __dict__ 中
d = Descriptor()在这一层面上,d 与任何普通对象并无区别。
(2)描述符语义的由来
描述符之所以获得特殊语义,并非源于其“身份”,而在于其实现了特定的协议方法,并且位于类属性位置。
当一个对象同时满足以下条件时,在属性访问过程中,就会被解释器识别为描述符对象:
• 实现 __get__()、__set__()、__delete__() 中至少一个
• 作为类属性存在于另一个类的 __dict__ 中
二、描述符的存储位置与作用范围
(1)描述符的存储位置
描述符对象在参与属性访问控制时,必须作为类属性存在于另一个类对象的 __dict__ 中。
x = D() # 描述符对象存放在 A.__dict__ 中此处的 D() 是一个普通对象,但由于它位于 A.__dict__ 中,因此进入属性查找链。
(2)描述符的作用对象
尽管描述符存在于类级别,但其控制的却是:
• 实例属性的访问
• 类属性的访问行为(当 instance is None)
比如:
print(a.x) # 输出:descriptor因为该访问会被解释为:
A.__dict__['x'].__get__(a, A)从语言规范角度看,描述符对象本质上是对的实现。这些协议方法不是“魔法”,而是 Python 在属性查找过程中主动调用的标准接口。
三、描述符对象的分类
根据是否拦截属性写入或删除操作,描述符可分为两类:数据描述符(Data Descriptor)和非数据描述符(Non-data Descriptor)。
(1)数据描述符
定义:实现了 __set__() 和 / 或 __delete__(),通常同时实现了 __get__() 方法。
行为特征:在属性查找顺序中优先级高于实例 __dict__,因此实例无法通过同名属性绕过其控制。
示例:
obj.__dict__[self.storage_name] = value作为类属性使用:
balance = Positive()访问行为验证:
print(a.__dict__) # 输出 {'_balance': 100, 'balance': -999}在 Python 的世界里,没有什么能完全阻止一个想要直接操作 __dict__ 的开发者,但描述符能确保通过“正规途径”(即 a.balance = val)进入的数据一定是合法的。真正的保护应将存储名(如 _balance)与属性名(balance)分离。
(2)非数据描述符
定义:仅实现 __get__() 方法。
行为特征:优先级低于实例 __dict__,因此可被实例属性遮蔽。
示例:
return value作为类属性使用:
return 42访问行为验证:
print(d.value) # 第二次:直接从 d.__dict__ 取值,42,不再触发描述符以上示例利用非数据描述符优先级低于实例 __dict__ 的特性实现“惰性求值”:首次访问时触发计算并将结果缓存至实例 __dict__ ;后续访问则因实例属性“遮蔽”了描述符而直接读取缓存,从而有效避免重复计算,优化运行性能。
四、Python 内置的描述符对象
Python 中的大量核心对象,本身就是描述符对象。
(1)函数对象:非数据描述符
类中定义的函数对象本身是非数据描述符。通过其 __get__() 方法,Python 实现了实例方法的自动绑定。
a = A()当访问方法 foo:
a.foo本质是:
A.__dict__['foo'].__get__(a, A)从而生成绑定方法(Bound Method)。
(2)@property:标准数据描述符
@property 返回的是标准的数据描述符对象(实现了 __get__()、__set__() 和 __delete__()),用于将属性访问映射为函数调用。
示例:
self._age = value访问行为:
p.age = 30 # 调用 property.__set__可以这样说,@property 是描述符机制的官方封装版本。
(3)@classmethod 与 @staticmethod
这两个装饰器均返回描述符对象,分别实现对类对象或函数本身的不同绑定策略。
示例:
return "no binding"访问验证:
print(Demo().static_method()) # 仍不绑定classmethod 的描述符在 __get__() 中绑定 owner。staticmethod 的描述符在 __get__() 中直接返回函数。二者都是描述符对象,只是绑定策略不同。
五、描述符对象在属性查找链中的位置
当执行 obj.attr 时,Python 的查找顺序为:
1、类 __dict__ 中的数据描述符
2、实例 obj.__dict__
3、类 __dict__ 中的非数据描述符
4、类 __dict__ 中的普通属性
5、__getattr__() 方法
描述符的“权力”并非绝对,而是由协议与顺序共同决定的。
六、描述符的现代最佳实践:__set_name__
Python 3.6 之后,引入了:
__set_name__(self, owner, name)__set_name__() 方法在类创建阶段被自动调用,使描述符对象能够获知自身的属性名与所属类。这是当前描述符实现的标准范式。
示例:
setattr(obj, self.storage_name, value)描述符作为类属性使用:
salary = Typed()此时在类创建过程中,解释器会隐式执行:
Typed.__set_name__(Employee, "salary")实际访问行为如下:
print(e.salary) # 输出:8000底层状态:
e.__dict__ == {"_age": 30, "_salary": 8000}实际数据存储在实例的 __dict__ 中,而访问路径始终经过类 __dict__ 中的描述符对象。
上例说明:
• Typed() 本身是一个普通对象。
• 它存在于 Employee.__dict__。
• 通过 __set_name__ 获得属性名。
• 通过 __get__ / __set__ 管理实例数据。
• 实例并不直接暴露真实存储字段。
这一结构正是现代描述符实现的标准范式,也是 ORM、字段系统、类型系统中最常见的设计基础。
小结
描述符对象是 Python 属性系统中的关键组成部分。它们以普通对象的形式存在于类 __dict__ 中,通过实现特定协议方法参与属性查找过程,从而实现对属性访问行为的精细控制。理解描述符,有助于全面把握 Python 对象模型与属性机制的设计思想。
![]()
“点赞有美意,赞赏是鼓励”
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.