尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Python中的类与对象之描述符详解
前言描述符descriptor是 Python 对象模型里被误解最多的一环。很多资料把它讲成「一种高级技巧」其实它就在你每天写的代码里函数、property、classmethod、staticmethod全都是描述符。obj.method之所以能自动带上self靠的就是函数这个非数据描述符的__get__。第一个要澄清的误解是「描述符和property是两回事」。实际上property就是标准库提供的一个数据描述符property只是它的装饰器门面。第二个误解是「描述符定义在哪个类里都行」。官方规则很硬描述符必须出现在拥有者类的类字典里或它的某个父类的类字典里把它存进实例字典是没用的。本文讲四件事描述符协议本身、数据描述符与非数据描述符的查找优先级、函数与property为什么属于不同类别、以及用它实现字段校验的完整例子。所有行为以 Python 官方数据模型章节为准本机无解释器输出为人工推演。一、协议与一次属性查找一个对象只要定义了__get__/__set__/__delete__中的任意一个它就算描述符。三个方法的签名是__get__(self, instance, ownerNone) # 读instance 是访问它的对象类访问时为 None__set__(self, instance, value) # 写给实例上的这个属性赋值__delete__(self, instance) # 删del 实例上的这个属性最简例子# 适用于 Python 3.0class Descriptor:def __get__(self, instance, ownerNone):return 从描述符读取def __set__(self, instance, value):print(收到赋值:, value)class C:x Descriptor() # 关键放在类字典里c C()print(c.x) # 从描述符读取c.x 10 # 收到赋值: 10print(C.x) # 从描述符读取 ← 类访问时 instance 为 None官方把描述符参与的查找过程写得很明确读a.x时相当于调用type(a).__dict__[x].__get__(a, type(a))读类属性A.x时相当于调用A.__dict__[x].__get__(None, A)如果是super(A, a).x则从a的 MRO 里A之后找基类B调用B.__dict__[x].__get__(a, A)。所以__get__的owner参数是「查找起点所在的类」不一定等于type(instance)。二、查找优先级数据描述符 vs 非数据描述符这是描述符最容易答错的部分。按官方定义定义了__set__或__delete__的叫数据描述符data descriptor只定义__get__的叫非数据描述符non-data descriptor。实例属性查找的优先级从高到低是优先级来源说明1数据描述符在类及父类的字典里永远压过实例字典2实例的__dict__普通属性就存在这里3非数据描述符在类及父类的字典里会被实例字典覆盖4类属性普通值用两段代码看清差别# 适用于 Python 3.0class NonData:def __get__(self, instance, ownerNone):return 来自非数据描述符class Data:def __get__(self, instance, ownerNone):return 来自数据描述符def __set__(self, instance, value):instance.__dict__[_hidden] valueclass A:d NonData()class B:d Data()a A()a.__dict__[d] 来自实例字典print(a.d) # 来自实例字典 ← 非数据描述符被实例字典覆盖b B()b.__dict__[d] 来自实例字典print(b.d) # 来自数据描述符 ← 数据描述符赢一句话记忆数据描述符有「写」的能力所以它有资格压住实例字典非数据描述符只有「读」实例字典一出现就把它盖住。三、函数、property、classmethod 分别属于哪一类官方明确说明函数包括被staticmethod、classmethod装饰的都是非数据描述符。这解释了为什么实例可以「覆盖」方法# 适用于 Python 3.0class C:def m(self):return 方法c C()print(c.m()) # 方法print(type(C.__dict__[m])) # class functionc.__dict__[m] 被覆盖print(c.m) # 被覆盖 ← 非数据描述符被实例字典压住c.m之所以是绑定方法是因为函数这个非数据描述符的__get__返回了一个「绑定方法对象」把c和函数绑在一起。同理classmethod的__get__绑定的是类staticmethod的__get__原样返回函数。这三种行为差异全部来自各自__get__的实现不同。而property是数据描述符所以它反过来能压住实例字典# 适用于 Python 3.0class C:propertydef p(self):return 42c C()c.__dict__[p] 99 # 直接塞进实例字典print(c.p) # 42 ← property 是数据描述符赢这也解释了官方那句「实例无法覆盖 property 的行为却可以覆盖普通方法」——差别就在数据描述符与非数据描述符。还有一个边界条款值得记住如果描述符只定义了__set__却没有__get__那么读这个属性时会返回描述符对象本身除非实例字典里正好有值。只写不读的描述符几乎没人这么用但规则确实如此。四、实战带类型校验的字段把__set_name__和描述符结合可以做出「自己知道名字、还会校验类型」的字段。这是不少 ORM 和表单库的基础# 适用于 Python 3.6class Typed:def __init__(self, expected_type):self.expected_type expected_typeself.name Nonedef __set_name__(self, owner, name):self.name name # 类创建时自动告知名字def __get__(self, instance, ownerNone):if instance is None: # 类访问返回描述符本身return selfreturn instance.__dict__.get(self.name)def __set__(self, instance, value):if not isinstance(value, self.expected_type):raise TypeError(f{self.name} 需要 {self.expected_type.__name__}f收到 {type(value).__name__})instance.__dict__[self.name] valuedef __delete__(self, instance):del instance.__dict__[self.name]class Person:name Typed(str)age Typed(int)def __init__(self, name, age):self.name name # 走 Typed.__set__self.age agep Person(Alice, 30)print(p.name, p.age) # Alice 30p.age 不是整数 # TypeError: age 需要 int收到 str三个要点__set_name__Python 3.6由type.__new__在建类时自动调用参数是所属类和属性名于是字段不必自己写名字。官方提醒类建好之后再给类属性赋值钩子不会自动触发需要时得手动调__set_name__。真实数据存在instance.__dict__[self.name]所以读的时候必须走__get__不能指望实例字典被自动查找到——因为它是数据描述符优先级最高的那一档。instance is None时返回self这样Person.name能拿到描述符对象本身便于做内省。少了这个分支类访问就会因为对None取属性而报错。另外描述符对象还可以带上__objclass__属性inspect模块会把它当作「这个对象定义在哪个类里」的提示写通用工具时用得上。常见坑点把描述符实例存进实例而不是类❌c.d Descriptor()→ 它只是实例属性协议完全不生效c.d直接返回那个对象。 ✅ 描述符必须写在类体里class C: d Descriptor()。以为非数据描述符能压住实例属性❌ 只定义__get__的自定义描述符却指望实例字典里的同名值被忽略。 ✅ 记住非数据描述符排在实例字典之后要压住就得同时定义__set__。__get__没处理instance is None❌def __get__(self, instance, ownerNone): return instance.attr→ 类访问C.x时instance是None直接AttributeError。 ✅ 先判断if instance is None: return self。在__get__里用getattr(instance, self.name)取值❌def __get__(self, instance, ownerNone): return getattr(instance, self.name)→ 读的正是同一个名字无限递归。 ✅ 走实例字典return instance.__dict__.get(self.name)。依赖__set_name__在类建好后仍会自动调用❌class A: pass之后A.x Typed(int)→ 钩子不会再触发self.name还是None。 ✅ 手动补一句A.x.__set_name__(A, x)或把字段写在类体里。__set__里直接写self.xxx期望存到实例上❌def __set__(self, instance, value): self.value value→ 值存到了描述符对象自己的属性上被所有实例共享。 ✅ 存进实例字典instance.__dict__[self.name] value。只写__set__不写__get__❌ 以为读的时候会返回存进去的值 → 实际返回描述符对象本身实例字典里恰好有值才例外。 ✅ 读写都要就两个都定义。描述符只影响实例不影响类字典本身❌C.x 1之后指望实例的c.x还是走__get__→ 类字典里的描述符已被值替换协议不再生效。 ✅ 需要动态改行为就改描述符内部状态而不是把类属性整个换掉。另注Python 2 里旧式类class C:不继承object对描述符支持不完整Python 2.7 已于 2020 年 1 月 1 日停止维护Python 3 里所有类都是新式类上述规则一致适用。总结概念判定与实例字典比数据描述符定义了__set__或__delete__描述符赢非数据描述符只定义__get__实例字典赢函数 /classmethod/staticmethod非数据描述符实例可覆盖property数据描述符实例不可覆盖查找起点描述符必须在类或父类字典里实例字典里的描述符无效描述符的全部秘密就是「属性查找按优先级选出一个对象再按它的协议决定返回什么」。记住数据描述符与非数据描述符这条分界线obj.method的绑定、property的只读、classmethod与staticmethod的差别就都能用同一套规则推出来。
RELATED

相关推荐

Spring Boot读写分离实战:基于AbstractRoutingDataSource与AOP轻量实现

Spring Boot读写分离实战:基于AbstractRoutingDataSource与AOP轻量实现

简介:面向Spring Boot开发者的数据库读写分离实现指南,适合需要优化系统并发读性能、降低主库压力的中高级Java工程师。资源为单个PDF文档,大小约48KB,涵盖从主从库配置到代码层动态路由的完整思路。内容以AbstractRoutingDataSou…

📅 2026/10/9 11:24:45
意大利艺术涂料值不值得做?从材料原理到施工避坑全解析

意大利艺术涂料值不值得做?从材料原理到施工避坑全解析

装修界这几年的风向转得很快,前几年大家还在纠结乳胶漆刷什么颜色、墙纸选什么花纹,现在越来越多的业主设计师一开口就问:意大利艺术涂料到底值不值得做?我在项目里跟艺术涂料打交道也有七八年了,经手过从几百平的大平…

📅 2026/10/9 11:24:45
Java多线程进阶:Thread属性、线程中断与join方法实战排查指南

Java多线程进阶:Thread属性、线程中断与join方法实战排查指南

1. 从线程的“身份档案”说起:属性能告诉我们什么很多人在刚开始接触Java多线程时,习惯把Thread类当成一个“能跑东西的对象”来用:new一个Thread,重写run(),start(),完事。但对Thread本身自带的那一堆属性…

📅 2026/10/9 11:24:45
MORE NEWS

更多资讯

📰

DeepSeek Harness 插件迁移 Claude Code Mods 权限兼容层实战

1. 从一个真实踩坑说起:插件迁移了,权限为什么没跟着走前段时间我在折腾 DeepSeek Harness 接入 Claude Code Mods 的时候,遇到一个特别典型的场景:插件文件全部拷贝过去了,配置文件也照搬了,启动日志看起来…

📰

自动化测试失败自动截图与日志捕获机制落地实践

测试跑挂了,最让人头疼的不是红字本身,而是当你盯着屏幕想定位问题时,发现日志早就被刷屏冲走了,截图也压根没留。自动化测试执行频率越高、用例规模一大,这种情况就越致命——失败信息如果不能在第一时间完整捕获&…

📰

软件质量保障体系实战:从测试金字塔到CI/CD质量门禁

干开发这行十几年,我见过太多项目的死法:不是死在需求改版,不是死在加班太少,而是死在“质量这根弦崩得太晚”。很多团队对“软件质量保障”的理解还停留在“测试同学加班点点点”,结果等真正上线那一刻,崩…

📰

整套相机退坑出手怎么选回收平台?金典拍拍一站式解决方案

不同的闲置相机处理需求,适配的渠道并不一样。退坑出清整套器材、置换升级新机、处理高价值专业设备,对应的核心诉求差异很大。 针对摄影玩家常见的三类场景,我们结合金典拍拍的服务模式,讲讲对应的解决方案。 一、场景一&#xf…

📰

Qt构建缓存导致AI优化不生效?拆解qmake/moc/清理机制与终极重建方案

有没有遇到过这种情况:AI 给你改好了一版 Qt 代码,逻辑清清楚楚,注释写得比人还细,你满怀信心地点了“构建运行”,结果程序行为跟改之前一模一样——没有报错,也没有崩溃,就是“像没改过一样”。…

📰

机器学习房价预测:回归模型与特征工程全指南

简介:面向人工智能、深度学习及Python相关课程设计与毕业设计场景,这份基于机器学习的房价与二手房房价预测项目源码,涵盖数据采集、预处理、特征分析、模型训练与评估的完整流程,适合即将完成期末大作业或希望进行项目实战的计算…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬