在 Python 面向对象编程中,类属性的访问和控制非常灵活。为了支持属性的动态访问、验证、缓存和代理行为,Python 提供了描述符协议(Descriptor Protocol),是 Python 中实现访问控制、延迟加载、ORM 字段、property 装饰器等机制的核心基础。
描述符提供了比传统 getter/setter 更统一、更强大的能力,是 Python 对象模型中最重要的机制之一。理解描述符,能够让你真正掌握 Python 的属性访问机制。
一、传统的 Setter/Getter/Deleter
假设我们有一个类 BankAccount,希望控制银行账户余额 _balance 的访问方式:
del self._balance使用方式:
# account.set_balance(-50) # 会抛出 ValueError为什么说这是“传统的”?
• 必须显式调用 get_x / set_x,不够 Pythonic。
• 无法用自然的属性语法(obj.attr)。
• 冗长、繁琐,不符合“简单即美”的 Python 哲学。
因此,Python 提供了更高级的方案 —— @property。
二、Python 的进化:@property 装饰器
@property 允许你用属性语法访问方法:
del self._balance使用方式:
del account.balance # 输出:Deleting balance!我们也可以不使用装饰器语法,而是显式创建 property:
balance = property(get_balance, set_balance, del_balance)装饰器是语法糖,本质和显式写法等价!
三、描述符与描述符协议
property 不是魔法,它基于描述符协议(Descriptor Protocol)。
(1)什么是描述符
只要一个对象实现了以下任意方法,它就是描述符:
• __get__(self, instance, owner)
• __set__(self, instance, value)
• __delete__(self, instance)
并且当此对象作为类属性存在时,通过 obj.attr 访问会自动调用这些方法。描述符允许我们“钩住”属性访问过程,自定义其行为。
(2)描述符协议三方法
__get__(self, instance, owner)
读取属性时触发。
参数:
self:描述符实例本身。
instance:通过实例访问时为实例,通过类访问时为 None。
owner:拥有描述符的类(通过类访问时)。
__set__(self, instance, value)
设置属性时触发。
__delete__(self, instance)
删除属性时触发。
所有 property、方法绑定、ORM 字段、cached_property 都基于描述符协议。
(3)property 与描述符的关系
当我们使用 @property 时,本质上是在创建一个 property 描述符实例。
以下是 property 的简化原理:
self.fdel(instance)property 就是一个实现了完整描述符协议的类。
property 默认是“非数据描述符”;只有在定义了 fset 或 fdel 时才成为“数据描述符”。
四、数据描述符与非数据描述符
Python 将描述符分为两类,它们的优先级不同。
(1)数据描述符(Data Descriptor)
指的是定义了 __set__ 或 __delete__ 的描述符。
如:
• property(具有 setter 或 deleter)
• 自定义描述符实现了 __set__
• ORM 字段、typed 属性验证描述符
优先级:数据描述符优先于实例属性。
示例:
print(a.x) # get —— 实例属性不会覆盖数据描述符(2)非数据描述符(Non-Data Descriptor)
指的是只实现了 __get__ 的描述符。
如:
• 普通方法(function)
• property(无 setter / deleter)
优先级:实例属性优先于非数据描述符。
示例:
print(a.x) # 100 —— 实例字典覆盖了非数据描述符优先级总结:
数据描述符 > 实例属性 > 非数据描述符 > 普通类属性示例:
print(obj.non_data_desc) # 输出: "实例属性"(实例属性优先)五、属性查找顺序
当执行 obj.attr 时,Python 实际执行(简化逻辑):
1、在类(type(obj)) 中查找 attr;如果找到且是数据描述符 → 调用其 __get__ 并返回结果。
2、否则查找实例字典 obj.__dict__;如果存在 → 返回该值。
3、若类属性是非数据描述符 → 调用其 __get__ 并返回结果。
4、否则返回类属性本身。
5、若都找不到 → 如果对象实现了 __getattr__ 则调用它。
设置属性时:
• 若存在数据描述符 → 调用其 __set__
• 否则写入实例字典
删除属性时:
• 若存在数据描述符 → 调用其 __delete__
• 否则从实例字典删除
示例:完整的查找规则演示
print(demo.class_attr) # 类属性这种精细的属性查找顺序与描述符优先级机制,使得 Python 在实现面向对象特性时既灵活又高效。
六、自定义描述符
下面构造一个完整的年龄验证描述符,改进 __get__ 的容错处理,同时演示推荐的实例数据存储方式(使用实例字典并带上唯一键)并加入 __set_name__ 支持以获取属性名:
将其绑定到类属性:
使用示例:
(1)描述符实例是类属性,不是实例属性
描述符对象只创建一次(在类定义时),不应该在描述符内部用 self.xxx 来存储每个实例的数据,否则所有实例会共享同一份数据(通常这是错误的)。正确做法是将实例数据存储在 instance.__dict__ 或使用 instance 上的独立键(例如 _{name})。
(2)set_name(推荐)
自 Python 3.6 起,描述符可以实现 __set_name__(self, owner, name),类创建时会被调用一次,这可以帮助描述符自动记录它在类中对应的属性名,从而简化向 instance.__dict__ 中存储值的实现(见上面示例)。这是实现“按属性名存储”“不会冲突”的推荐方式。
(3)删除属性后再访问
del p.age 会调用 __delete__,如果我们在 __get__ 中直接访问 instance.__dict__ 中的键而该键不存在,会抛出 AttributeError。在实际实现中可以选择更友好的行为(例如返回 None、抛出带信息的异常或触发延迟加载)。
七、描述符的典型应用场景
描述符在 Python 中有广泛的应用。
(1)数据验证与类型检查
(2)延迟加载与缓存机制(如 cached_property)
(3)观察者模式与属性监听
(4)权限控制与访问审计
(5)ORM 与数据映射(字段描述符)
(6)配置管理与依赖注入
补充说明:方法如何绑定为 bound method
Python 中的函数对象(定义在类体中的函数)实现了 __get__(即函数对象是非数据描述符),当通过实例访问时,function.__get__(instance, owner) 会返回一个“绑定方法”(bound method),它把该实例作为第一个参数(self)封装到函数上。理解这一点有助于把“方法也是描述符”与前面描述符优先级的讨论连起来。
小结
描述符协议是 Python 中控制属性访问的底层机制。只要一个类实现了 __get__、__set__ 或 __delete__,它就能作为描述符拦截属性的读取、写入和删除操作。property、方法绑定、ORM 字段以及许多高级特性都依赖描述符协议。
理解描述符的关键在于区分数据描述符和非数据描述符,并掌握它们在属性查找顺序中的不同优先级。__set_name__ 是现代描述符实现中非常有用的钩子(Python 3.6+),推荐在自定义描述符中使用它来管理实例字典的键。
通过自定义描述符,我们可以构建高度可复用、可扩展的属性管理逻辑,使得类的行为更加灵活与优雅;描述符不仅是 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.