描述符协议(Descriptor Protocol)是 Python 属性访问机制中的一项核心规则。它规定了解释器在处理 obj.attr 等属性相关语法时,若在类属性层发现满足条件的对象,应如何将属性行为解释为对其协议方法的调用。
理解描述符协议,有助于从解释器视角把握 Python 属性访问机制的整体设计逻辑,而不是将其误解为某种“特殊对象”或“语法技巧”。
一、什么是描述符协议
1、为什么称为“描述符”
“描述符”(descriptor)这一名称,并不是指“描述属性值的对象”。更贴近的理解是:在属性相关语法中,解释器将属性行为的解释交由某个对象的协议方法处理,该对象因此承担描述属性行为的语义角色。
描述符关注的不是属性值,而是属性行为的解释规则。
2、描述符协议的方法构成
描述符协议关注以下三个约定方法在类型层是否存在,并在特定语法语境与解析阶段中决定是否调用它们:
__get__(self, instance, owner)__set__(self, instance, value)__delete__(self, instance)
这些方法本身只是普通函数对象,定义在类中,存放于类对象的命名空间里。
它们并不会因为名字特殊而自动生效,其语义是否成立,完全取决于解释器在属性解析过程中是否选择进入相应的处理分支。
二、协议触发的必要条件
描述符协议并不会在任意情况下被考虑,它的触发受到代码的语法结构与位置条件的共同约束。
1、语法触发
解释器仅在以下属性相关语法语境下,才会按照描述符协议的规则进行判定与分派:
• 属性访问:obj.attr
• 属性设置:obj.attr = value
• 属性删除:del obj.attr
在这些语法语境中,解释器并不会直接对实例字典进行操作,而是首先进入属性解析流程。
2、位置约束
在属性解析过程中,描述符协议的成立判定发生在对象的类型层级(类或元类)中。
示例:同一个对象在不同位置的行为差异
class D:def __get__(self, instance, owner):return 42
(1)在类属性位置
class A:x = D() # D() 在类属性位置a = A()print(a.x) # 42
说明:
(1)解释器首先确认 a.x 是一次属性访问语法。
(2)在类属性查找阶段检查 A.__dict__['x']
(3)判断该对象的类型是否实现了 __get__
(4)判定成立后,调用:
A.__dict__['x'].__get__(a, A)(2)在实例属性位置
class B:passb = B()b.x = D() # D() 仅存于实例字典print(b.x) # <__main__.D object at ...>
说明:
当一个对象仅存在于实例字典中时,解释器不会在协议判定阶段考虑它,描述符协议自然不成立。
三、属性访问的统一解析路径(描述符介入位置)
在不重写 __getattribute__ 的前提下,属性访问 obj.attr 的解析过程可以抽象为以下顺序:
该顺序是对 CPython 默认属性解析流程的抽象描述,用于说明描述符优先级关系;实际实现中,该流程统一由 __getattribute__ 驱动。
四、数据描述符与非数据描述符
描述符协议不仅决定是否介入属性解析,还决定介入时的优先级形态。这一差异并不是对象分类,而是解析规则中的优先级分化:
• 数据描述符:其类型实现了 __set__ 或 __delete__
• 非数据描述符:仅实现了 __get__
1、非数据描述符:可被实例属性遮蔽
示例:
class D:def __get__(self, instance, owner):return "from descriptor"class A:x = D()a = A()print(a.x) # from descriptora.x = "instance value"print(a.x) # instance value
说明:
(1)解释器确认 a.x 是一次属性访问语法。
(2)在类属性中找到 A.__dict__['x'],其类型(D 类)实现了 __get__。但该类型未实现 __set__,属于非数据描述符语义。
(3)描述符语义判定成立后,调用:
A.__dict__['x'].__get__(a, A)输出返回结果。
(4)接着,解释器确认 a.x = "instance value" 是一次属性设置语法。
(5)在类属性中检查 A.__dict__['x'],同样发现该位置上的对象仅具备非数据描述符语义。
(6)由于实例属性在属性解析顺序中优先于非数据描述符,此处的 a.x 被解释为实例的一个新属性,从而遮蔽了原有的非数据描述符语义。
(7)按解析顺序,实例字典优先,直接返回:
a.__dict__['x']这表明,实例字典中的同名属性不参与“是否属于描述符”的判定阶段,但可能在后续的解析优先级中覆盖非数据描述符的结果。
另外,在类体中定义的函数,本质上也是类对象的属性。由于 function 类型默认提供了 __get__ 方法,经由实例属性访问时,触发描述符协议分支(非数据描述符),都可能返回一个绑定方法对象。
示例:
class A:def f(self):return "method"a = A()print(a.f) # <bound method A.f of <__main__.A object at ...>>print(a.f()) # method
2、数据描述符语义:优先于实例属性
示例:
class D:def __get__(self, instance, owner):return instance._xdef __set__(self, instance, value):instance._x = valueclass A:x = D()a = A()a.x = 10print(a.x) # 10a.__dict__['x'] = 99print(a.x) # 10
说明:
(1)解释器确认 a.x 是一次属性访问语法。
(2)在类属性中检查 A.__dict__['x'],发现其类型实现了 __set__(数据描述符语义成立)
(3)数据描述符在解析顺序中优先,执行:
A.__dict__['x'].__get__(a, A)数据描述符之所以是“强约束”,完全来自解析顺序规则。
五、@property 与描述符协议的关系
@property 是一个以函数对象为输入、返回一个新的实例对象的装饰器。该实例由于其类型(property 类)默认实现了描述符协议方法,得以在属性相关语法中被解释器按规则调用。
示例:
class A:@propertydef x(self):return 10
在类定义阶段,上述代码等价于:
class A:def x(self):return 10x = property(x)
property(x) 类调用接收一个函数对象 x,然后返回一个新的对象(property 类型),该对象被绑定在 A.x 的类属性位置上。
property 类默认实现了描述符协议:
• __get__(始终存在)
• __set__(当定义了 setter 时存在)
• __delete__(当定义了 deleter 时存在)
因此:
• 只定义 getter 的 property,在属性访问语境下具备非数据描述符语义
• 定义了 setter 或 deleter 的 property,在解析顺序中具备数据描述符语义
示例:若为该属性定义了 setter
class A:@propertydef x(self):return 10@x.setterdef x(self, value):...
则在执行:
a.x = value时,解释器的处理路径为:
(1)确认这是一次属性赋值语法
(2)在类属性中检查 A.__dict__['x']
(3)判断该对象的类型是否实现了 __set__(成立)
(4)进入描述符协议分支,执行:
A.__dict__['x'].__set__(a, value)六、自定义描述符
自定义描述符并不是向解释器注册新能力,而是按照既有的描述符协议规则,提供一个可被解释器调用的实现。
自定义过程可以概括为:
1、定义一个普通类。
2、在该类中实现 __get__、__set__、__delete__ 中的一个或多个
3、将该类的实例绑定到宿主类的属性名上
4、由解释器在属性相关语法中决定是否调用这些方法
示例:属性校验型描述符(数据描述符语义)
class Positive:def __get__(self, instance, owner):return instance._valuedef __set__(self, instance, value):if value <= 0:raise ValueError("must be positive")instance._value = valueclass Product:price = Positive()
p = Product()p.price = 10print(p.price) # 10p.price = -5 # ValueError
自定义描述符的价值不在于引入新的能力,而在于将属性相关的解释规则集中封装为可复用的单元。这使属性校验、计算、约束等行为不必散落在 __getattribute__ 或业务逻辑中,而是以清晰、可预测的方式融入解释器既有的属性解析规则体系之内。
七、典型应用场景
描述符协议在 Python 生态中的典型应用包括:
1、@property:受控属性访问
2、ORM 字段(如 Django / SQLAlchemy):字段行为描述
3、属性校验与类型检查
4、延迟计算属性
5、只读或受限写入属性
这些场景的共同点在于:属性不再只是存储槽,而是由解释器通过协议规则解释的语义节点。
📘 小结
描述符协议不是对象模型中的实体,而是一组由解释器遵循的解析规则。它规定了解释器在属性相关语法中如何查找候选对象、如何判定是否启用协议处理路径,以及在多种可能行为并存时如何依据优先级作出选择。类属性位置上的对象只是规则的承载点,其是否承担描述符语义,完全取决于解释器对这些规则的执行结果。
