news 2026/5/20 2:56:39

Python:描述符对象

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 对象模型与属性机制的设计思想。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/9 0:53:11

本地运行大模型不再是梦:Anything-LLM部署避坑指南

本地运行大模型不再是梦&#xff1a;Anything-LLM部署避坑指南 在一台普通的家用电脑上&#xff0c;上传一份PDF技术文档&#xff0c;输入“帮我总结这篇论文的核心观点”&#xff0c;几秒后屏幕上逐字浮现精准回答——这听起来像科幻场景&#xff0c;但如今只需一个开源工具就…

作者头像 李华
网站建设 2026/5/19 7:10:34

释放大模型潜力:结合Token计费模式推广Anything-LLM服务

释放大模型潜力&#xff1a;结合Token计费模式推广Anything-LLM服务 在企业知识管理日益复杂的今天&#xff0c;如何让非技术团队也能轻松驾驭大语言模型&#xff08;LLM&#xff09;&#xff0c;同时避免算力资源被无节制消耗&#xff0c;已成为AI落地的关键瓶颈。一个典型的场…

作者头像 李华
网站建设 2026/5/10 17:22:25

缓存层引入Redis:减少重复计算开销

缓存层引入Redis&#xff1a;减少重复计算开销 在构建现代AI应用的过程中&#xff0c;一个看似微小却影响深远的问题逐渐浮现——用户反复提问相同内容时&#xff0c;系统是否每次都必须“从头开始”执行完整的检索与生成流程&#xff1f;尤其是在基于RAG&#xff08;Retrieva…

作者头像 李华
网站建设 2026/5/13 13:24:11

mysql.connector.errors.OperationalError: 1040 (08004): Too many connections

从报错截图来看&#xff0c;核心错误信息是&#xff1a; mysql.connector.errors.OperationalError: 1040 (08004): Too many connections 这意味着你的 Python 程序&#xff08;具体是在 Streamlit 框架下运行&#xff09;向 MySQL 数据库发起了过多的连接请求&#xff0c;超出…

作者头像 李华
网站建设 2026/5/1 8:15:46

写个简单的ros2代码

1、再主文件夹中右击鼠标打开终端&#xff0c;输入以下命令进入vscode mkdir -p demo_04/src cd demo_04 code .2、右击src选择在集成终端打开 输入 ros2 pkg create test111 --build-type ament_python --dependencies rclpy然后就能在src目录下看到 3、ok现在可以看到test下方…

作者头像 李华