python属性描述符本质是通过__get__/__set__/__delete__方法控制属性访问,依据查找顺序(类属性优先于实例属性)触发;数据描述符(含__set__)覆盖实例属性,非数据描述符(仅__get__)可被实例属性遮蔽。

Python 属性描述符(descriptor)的本质,是通过类的特殊方法(__get__、__set__、__delete__)控制对实例属性的访问逻辑。它不是语法糖,而是 Python 属性查找机制中明确参与的一环——当访问某个属性时,如果该属性是一个实现了至少一个描述符方法的对象,且被定义在类的命名空间中(而非实例字典里),解释器就会触发对应的方法。
描述符为什么能接管属性访问
关键在于 Python 的属性查找顺序(MRO 中的类属性优先于实例属性)。当执行 obj.attr 时,解释器按如下逻辑处理:
- 先在
obj.__class__.__dict__中查找attr - 如果找到且该对象有
__get__方法(即它是“非数据描述符”或“数据描述符”),就调用attr.__get__(obj, type(obj)) - 如果是“数据描述符”(同时定义了
__set__或__delete__),它会**覆盖**实例字典中的同名项(即obj.__dict__['attr']完全无效) - 只有当类中没找到描述符,才会去查
obj.__dict__
数据描述符 vs 非数据描述符的区别
这个区分直接影响属性行为,是理解 descriptor 的核心:
- 数据描述符:实现了
__set__或__delete__(哪怕只实现其中一个);它优先级最高,能屏蔽实例字典里的同名属性 - 非数据描述符:只实现了
__get__;当实例字典中存在同名 key 时,会直接返回实例值,跳过__get__
例如:Property 是数据描述符(内部含 __set__),所以 @property 定义的属性不能被实例赋值覆盖;而函数对象是典型的非数据描述符,所以方法调用走的是 func.__get__(obj, cls),但你仍可给实例设 obj.func = 42 来遮蔽它。
立即学习“Python免费学习笔记(深入)”;
手动写一个有用的描述符
比如实现一个带类型校验的属性:
class Typed: def __init__(self, expected_type): self.expected_type = expected_type self.name = None # 后续由 __set_name__ 填充(Python 3.6+) <pre class="brush:php;toolbar:false;">def __set_name__(self, owner, name): self.name = name def __set__(self, obj, value): if not isinstance(value, self.expected_type): raise TypeError(f'{self.name} must be {self.expected_type.__name__}') obj.__dict__[self.name] = value def __get__(self, obj, owner): if obj is None: return self return obj.__dict__.get(self.name)
使用
class Person: name = Typed(str) age = Typed(int)
p = Person() p.name = “Alice” # OK p.age = 30 # OK p.age = “30” # TypeError
注意:__set_name__ 是可选的,用于自动获取属性名,避免手动传参;实际存取仍通过实例字典完成,保证效率。
常见内置描述符和使用场景
很多 Python 特性底层都基于描述符:
-
property:最常用的数据描述符,封装 getter/setter/deleter -
classmethod和staticmethod:非数据描述符,影响绑定行为(cls/ 无绑定) - 函数对象本身:也是非数据描述符,所以
obj.method触发method.__get__(obj, cls)返回绑定方法 - 第三方库如
attrs、dataclasses内部也大量依赖描述符做字段管理
理解描述符,等于看懂了 Python “属性访问”这层抽象背后的执行链路——它不神秘,只是把控制权交给了开发者定义的对象。