Python描述符进阶:__get__/__set__协议与属性拦截底层深度解析
一、引言:当你写 obj.x = 1 时,Python 到底做了什么
很多 Python 用者以为 obj.x = 1 就是"往对象里塞一个叫 x 的字段"。其实在 C 层面,它走的是 type(obj).__setattr__(obj, 'x', 1),而 __setattr__ 在落盘之前会先去类型上找 x 是不是一个描述符——如果是,就把控制权交给描述符的协议方法。
一句话本质:描述符(descriptor)是 Python 用协议方法(__get__/__set__/__delete__)实现的"属性拦截器",它让"读/写一个属性"变成一次可定制的函数调用;而 C++ 没有语言级的属性拦截,只能靠 getter/setter 约定或元对象编译器(如 Qt 的 moc)来近似。
坑:描述符只认类属性(定义在类型上),定义在实例字典里的同名函数不会触发协议;而且"数据描述符"和"非数据描述符"在查找优先级上完全不同,混用会出莫名其妙的 bug。
下面用可运行代码把这套机制彻底拆开。
二、描述符协议:三类特殊方法
一个对象只要实现了下面这些方法中的一部分,就会被 Python 当成描述符:
__get__(self, instance, owner):访问属性时调用。instance是持有者实例,owner是所属类(通过类访问时为None)。__set__(self, instance, value):赋值时调用。__delete__(self, instance):删除时调用。
按"实现了哪些"分为两类:
- 数据描述符(data descriptor):同时实现
__get__和__set__(或__delete__)。它优先级最高,会盖过实例字典。 - 非数据描述符(non-data descriptor):只实现
__get__。优先级低于实例字典——实例一旦自己写了同名属性,描述符就被"遮蔽"。
1 | class ReadOnly: |
输出:
1 | fixed |
注意最后一行:通过类访问时 instance 是 None,描述符返回了自身——这是很多"为什么类属性是描述符对象而不是值"困惑的根源。
三、数据描述符 vs 非数据描述符:优先级玄机
属性查找时,Python 按下面顺序裁决(简化自 CPython 的 type.__getattribute__):
关键直觉:数据描述符压过实例字典,非数据描述符被实例字典压过。这就是为什么 property 一定是数据描述符——它必须能拦截赋值。
1 | class Validated: |
输出:
1 | 80 |
__set_name__ 是常被忽略的宝藏:Python 在类创建时会自动把描述符所在的属性名喂给它,省去手写字段名。
四、property 的底层就是描述符
property 不是魔法,它只是用 C 实现的数据描述符。下面两者等价:
1 | class Celsius: |
为什么 property 能拦截赋值:因为它内部实现了 __set__,属于数据描述符,优先级高于实例字典,所以 c.temp = 999 一定走你的校验逻辑,而不是悄悄写进 c.__dict__['temp']。
五、实战:用描述符打造轻量 ORM 字段校验
把上面的 Validated 升级成通用字段,可直接用于轻量 ORM / 配置对象:
1 | class Field: |
输出:
1 | 7 老周 29 |
整个校验逻辑零样板重复——新增字段只需一行声明,这正是 Django ORM、SQLAlchemy 等框架字段系统的雏形。
六、C++ 视角对照与总结
C++ 没有描述符协议,属性访问拦截只能靠约定或外部工具:
| 能力 | Python(描述符) | C++ |
|---|---|---|
| 语言级属性拦截 | ✅ 协议方法 __get__/__set__ |
❌ 无原生属性语法 |
| 典型写法 | self.x = Desc() 定义在类上 |
手写 getX() / setX(v) 方法 |
| 运行时可定制 | ✅ 同一描述符跨类复用、可动态换 | ⚠️ 编译期固定,靠模板/CRTP 做静态拦截 |
| 声明式字段校验 | ✅ Field(int, min_=0) 一行 |
⚠️ 需宏或元对象编译器 |
| 最接近的对等物 | —— | Qt 的 Q_PROPERTY(moc 生成 READ/WRITE/NOTIFY) |
C++ 的两种近似方案:
- 手写 getter/setter:最普遍,但每个字段都是样板;且调用方写
obj.getX()而非obj.x,语义不透明。 - Qt 的
Q_PROPERTY+ moc:用宏声明Q_PROPERTY(int x READ getX WRITE setX NOTIFY xChanged),moc 在编译期生成访问器与信号——这是 C++ 里最像描述符的东西,但它依赖独立的元对象编译器,不是语言核心机制,且只服务于 Qt 生态。
C++20/23 有过"原生属性(properties)"提案,但至今未入标准。换句话说,Python 用运行时协议把"属性即方法调用"这件事做成了语言一等公民,而 C++ 仍把它留在约定与工具链里。这正是 Python 在 ORM、序列化、依赖注入等"声明式框架"领域开发体验更顺滑的根本原因。
小结
- 描述符 = 类属性上的协议对象,
__get__/__set__/__delete__决定读写语义。 - 数据描述符(有
__set__)压过实例字典;非数据描述符(仅__get__)被实例字典遮蔽——这是 90% 诡异 bug 的来源。 property本质是 C 实现的数据描述符;__set_name__让字段自感知名字。- 用它做字段校验,零样板、可跨类复用,是 ORM 类框架的基石。
- 对照 C++:无语言级属性拦截,靠 getter/setter 或 Qt moc 近似;Python 把"属性即调用"做成了一等公民。
收敛一句:会写
@property只是入门,懂"描述符协议 + 优先级裁决"才算真正握住 Python 属性系统的方向盘 🐾

