当前位置: 首页 > news >正文

Python:描述符对象

在 Python 的对象模型中,描述符对象(Descriptor Objects)是支撑语言动态特性的核心机制之一。从最基础的属性访问,到复杂的元编程框架(如 Django ORM、SQLAlchemy、Pydantic 的字段系统),描述符始终处于幕后,却决定着属性系统的最终行为。

如果说 __dict__ 体系提供了属性数据的静态存储结构,那么描述符对象就是介入这一结构之上的动态访问控制层。

需要强调的是,描述符不是特殊语法或内建魔法,而是完全遵循 Python 对象模型的普通对象。

一、描述符对象的概念

(1)描述符是对象

在 Python 中,一切皆对象。描述符也不例外:

• 它是某个类的实例

• 拥有自身的类型、属性与方法

• 可以被赋值、传递并存储于 __dict__ 中

class Descriptor: pass d = Descriptor()

在这一层面上,d 与任何普通对象并无区别。

(2)描述符语义的由来

描述符之所以获得特殊语义,并非源于其“身份”,而在于其实现了特定的协议方法,并且位于类属性位置。

当一个对象同时满足以下条件时,在属性访问过程中,就会被解释器识别为描述符对象:

• 实现 __get__()、__set__()、__delete__() 中至少一个

• 作为类属性存在于另一个类的 __dict__ 中

二、描述符的存储位置与作用范围

(1)描述符的存储位置

描述符对象在参与属性访问控制时,必须作为类属性存在于另一个类对象的 __dict__ 中。

class D: def __get__(self, obj, owner): return "descriptor" class A: x = D() # 描述符对象存放在 A.__dict__ 中

此处的 D() 是一个普通对象,但由于它位于 A.__dict__ 中,因此进入属性查找链。

(2)描述符的作用对象

尽管描述符存在于类级别,但其控制的却是:

• 实例属性的访问

• 类属性的访问行为(当 instance is None)

比如:

a = A()print(a.x) # 输出:descriptor

因为该访问会被解释为:

A.__dict__['x'].__get__(a, A)

从语言规范角度看,描述符对象本质上是对描述符协议的实现。这些协议方法不是“魔法”,而是 Python 在属性查找过程中主动调用的标准接口。

三、描述符对象的分类

根据是否拦截属性写入或删除操作,描述符可分为两类:数据描述符(Data Descriptor)和非数据描述符(Non-data Descriptor)。

(1)数据描述符

定义:实现了 __set__() 和 / 或 __delete__(),通常同时实现了 __get__() 方法。

行为特征:在属性查找顺序中优先级高于实例 __dict__,因此实例无法通过同名属性绕过其控制。

示例:

class Positive: """数据描述符:确保值为非负数""" def __set_name__(self, owner, name): # 将存储名设为 _balance self.storage_name = f"_{name}" def __get__(self, obj, owner): if obj is None: return self # 从私有备份中取值 return obj.__dict__.get(self.storage_name) def __set__(self, obj, value): if value < 0: raise ValueError("值必须为非负数!") # 存入私有备份 obj.__dict__[self.storage_name] = value

作为类属性使用:

class Account: balance = Positive()

访问行为验证:

a = Account()a.balance = 100 # 调用 Positive.__set__print(a.balance) # 调用 Positive.__get__,输出 100# a.balance = -100 # ValueError: 值必须为非负数! a.__dict__['balance'] = -999 # 这是“干扰项”print(a.balance) # 依然输出 100!print(a.__dict__) # 输出 {'_balance': 100, 'balance': -999}

说明:

在 Python 的世界里,没有什么能完全阻止一个想要直接操作 __dict__ 的开发者,但描述符能确保通过“正规途径”(即 a.balance = val)进入的数据一定是合法的。真正的保护应将存储名(如 _balance)与属性名(balance)分离。

(2)非数据描述符

定义:仅实现 __get__() 方法。

行为特征:优先级低于实例 __dict__,因此可被实例属性遮蔽。

示例:

class LazyValue: """非数据描述符:首次访问时计算""" def __init__(self, func): self.func = func def __get__(self, obj, owner): if obj is None: return self value = self.func(obj) # 将结果写入实例字典 obj.__dict__[self.func.__name__] = value return value

作为类属性使用:

class Data: @LazyValue def value(self): print("computing...") return 42

访问行为验证:

d = Data() print("第一次:")print(d.value) # 第一次:触发描述符,输出 "computing..." 42print("第二次:")print(d.value) # 第二次:直接从 d.__dict__ 取值,42,不再触发描述符

说明:

以上示例利用非数据描述符优先级低于实例 __dict__ 的特性实现“惰性求值”:首次访问时触发计算并将结果缓存至实例 __dict__ ;后续访问则因实例属性“遮蔽”了描述符而直接读取缓存,从而有效避免重复计算,优化运行性能。

四、Python 内置的描述符对象

Python 中的大量核心对象,本身就是描述符对象。

(1)函数对象:非数据描述符

类中定义的函数对象本身是非数据描述符。通过其 __get__() 方法,Python 实现了实例方法的自动绑定。

class A: def foo(self): pass a = A()

当访问方法 foo:

a.foo

本质是:

A.__dict__['foo'].__get__(a, A)

从而生成绑定方法(Bound Method)。

(2)@property:标准数据描述符

@property 返回的是标准的数据描述符对象(实现了 __get__()、__set__() 和 __delete__()),用于将属性访问映射为函数调用。

示例:

class Person: def __init__(self, age): self._age = age @property def age(self): return self._age @age.setter def age(self, value): if value < 0: raise ValueError("age must be >= 0") self._age = value

访问行为:

p = Person(20)print(p.age) # 调用 property.__get__p.age = 30 # 调用 property.__set__

可以这样说,@property 是描述符机制的官方封装版本。

(3)@classmethod 与 @staticmethod

这两个装饰器均返回描述符对象,分别实现对类对象或函数本身的不同绑定策略。

示例:

class Demo: x = 10 @classmethod def cls_method(cls): return cls.x @staticmethod def static_method(): return "no binding"

访问验证:

print(Demo.cls_method()) # cls -> Demoprint(Demo().cls_method()) # cls -> Demo print(Demo.static_method()) # 不绑定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__() 方法在类创建阶段被自动调用,使描述符对象能够获知自身的属性名与所属类。这是当前描述符实现的标准范式。

示例:

class Typed: def __set_name__(self, owner, name): # 在类创建阶段自动调用 self.storage_name = f"_{name}" def __get__(self, obj, owner): if obj is None: return self return getattr(obj, self.storage_name) def __set__(self, obj, value): if not isinstance(value, int): raise TypeError("Value must be int") setattr(obj, self.storage_name, value)

描述符作为类属性使用:

class Employee: age = Typed() salary = Typed()

此时在类创建过程中,解释器会隐式执行:

Typed.__set_name__(Employee, "age")Typed.__set_name__(Employee, "salary")

实际访问行为如下:

e = Employee()e.age = 30 # 调用 Typed.__set__(e, 30)e.salary = 8000 # 调用 Typed.__set__(e, 8000) print(e.age) # 输出:30print(e.salary) # 输出:8000

底层状态:

e.__dict__ == {"_age": 30, "_salary": 8000}

实际数据存储在实例的 __dict__ 中,而访问路径始终经过类 __dict__ 中的描述符对象。

上例说明:

• Typed() 本身是一个普通对象。

• 它存在于 Employee.__dict__。

• 通过 __set_name__ 获得属性名。

• 通过 __get__ / __set__ 管理实例数据。

• 实例并不直接暴露真实存储字段。

这一结构正是现代描述符实现的标准范式,也是 ORM、字段系统、类型系统中最常见的设计基础。


📘 小结

描述符对象是 Python 属性系统中的关键组成部分。它们以普通对象的形式存在于类 __dict__ 中,通过实现特定协议方法参与属性查找过程,从而实现对属性访问行为的精细控制。理解描述符,有助于全面把握 Python 对象模型与属性机制的设计思想。

“点赞有美意,赞赏是鼓励”

http://www.jsqmd.com/news/131151/

相关文章:

  • 大模型杀不死产品经理,但未来我们可能要做“产品界的 OnlyFans”
  • 教育培训认证体系:培养专业部署技术人员
  • SpringBoot+Vue Spring高校实习信息发布网站平台完整项目源码+SQL脚本+接口文档【Java Web毕设】
  • 迪桑特全球旗舰店“未来之城“于北京华贸正式揭幕 | 美通社头条
  • RS232电平转换电路设计:超详细版硬件实现指南
  • 按使用量付费模式:比买断制更适合中小企业
  • 【2025最新】基于SpringBoot+Vue的网上蛋糕售卖店管理系统管理系统源码+MyBatis+MySQL
  • 大模型训练算法宝典:6种主流算法对比与选择
  • 如何将PDF、Word文档变成可对话的知识源?试试Anything-LLM
  • 合作伙伴计划推出:招募代理商扩大市场覆盖
  • 缓存层引入Redis:减少重复计算开销
  • 日程安排建议:智能协调多方时间空档
  • SpringBoot+Vue spring电影订票系统管理平台源码【适合毕设/课设/学习】Java+MySQL
  • 一键部署Anything-LLM,快速接入GPU算力与Token服务
  • 投资回报率测算:部署anything-llm能省多少钱?
  • 提供试用版下载:降低用户决策门槛的有效手段
  • 国内主流云厂商合作:华为云、阿里云镜像同步
  • 大模型+RAG+Text2SQL:应急管理安全生产智能问答系统实战全流程
  • Agent驱动的工作流开发新范式:颠覆传统编程,效率提升10倍
  • 如何在线将音频转文字?在线免费音频文字识别教程
  • 前后端分离Sringboot+个人驾校预约管理系统系统|SpringBoot+Vue+MyBatis+MySQL完整源码+部署教程
  • screen 与 systemd 集成的后台服务部署方案
  • 基础设施即代码:Terraform部署anything-llm模板
  • EasyEDA平台下嘉立创PCB布线核心要点解析
  • ROI提升策略:最大化AI系统的商业价值
  • GitHub Star增长技巧:吸引更多开发者关注
  • 免费额度吸引用户:先体验后购买的营销逻辑
  • 2025企业即时通讯软件横评:4款主流私有化部署方案深度对比
  • 深入解析Firebase规则配置中的常见错误
  • 服务器的windows和Linux系统有什么区别