在 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 的底层机制,更是构建大型系统与框架的基础组件。

打开网易新闻 查看精彩图片

点赞有美意,赞赏是鼓励