bigywater.tech
Python · 9

描述器 Descriptor:property 和绑定方法背后的机制

实现一个数据描述器拦截属性读写,验证即使直接往实例__dict__里塞同名key,读取依然会被描述器拦截,看property是怎么用这套协议实现的。

拦截读写
数据描述器拦截每一次属性访问
优先级最高
盖过同名的实例__dict__键
property=描述器
验证property本身也是数据描述器

做的事:自己实现一个数据描述器,拦截属性读写

描述器就是实现了__get__/__set__的类,放在另一个类的类属性位置上。

代码class LoggedAttr: def __get__(self, obj, objtype=None): print(f"[描述器] 拦截了对 {self.name} 的读取") return obj.__dict__.get(f'_{self.name}') def __set__(self, obj, value): print(f"[描述器] 拦截了对 {self.name} 的写入: {value}") obj.__dict__[f'_{self.name}'] = value class Point: x = LoggedAttr('x') y = LoggedAttr('y')

验证:数据描述器拦截了每一次读写

对p.x的每次读写,都要经过描述器。

输出p = Point(1, 2) [描述器] 拦截了对 x 的写入: 1 ← __init__里self.x=x [描述器] 拦截了对 y 的写入: 2 p.x [描述器] 拦截了对 x 的读取 → 1 p.x = 100 [描述器] 拦截了对 x 的写入: 100

Point类上的 x 和 y 不是普通的类属性,而是 LoggedAttr 的实例——每次对 p.x 的读或写,都会被自动转发给 LoggedAttr.__get__ 或 __set__,实际的值存在 p.__dict__['_x'] 里,不是 p.__dict__['x']。

验证:数据描述器的优先级,高于实例的__dict__

如果直接往实例字典里塞一个同名的键,会发生什么。

结果p.__dict__['x'] = 999 # 直接往实例字典里塞了一个"x" p.x [描述器] 拦截了对 x 的读取 → 100 ← 还是描述器给出的值,不是999!

哪怕往 p.__dict__ 里塞进了一个名叫 x 的键(值是999),读 p.x 依然走的是描述器的 __get__,返回的是描述器自己维护的100,那个999完全被晾在一边没人用。这就是"数据描述器"(同时定义了 __get__ 和 __set__)的优先级规则:类上的数据描述器,会盖过同名的实例属性——这也是Python属性查找里比"先查实例字典"更靠前的一条规则。

验证:property本身就是这套协议的一个应用

@property装饰出来的东西,也是一个描述器。

结果type(Celsius.temperature) → :<class 'property'> hasattr(Celsius.temperature, '__get__') → :True hasattr(Celsius.temperature, '__set__') → :True c = Celsius(25) c.temperature = -300 拦截了非法赋值: 低于绝对零度,不合法

验证了 property 类型本身就同时实现了 __get__ 和 __set__——它就是一个由C语言写好的、通用的数据描述器,@property 装饰器做的事,就是把你写的getter/setter函数包进一个property对象,挂到类属性上,跟本文前面手写的 LoggedAttr 是同一套机制,只是property更通用、更贴近日常写代码的方式。

分步演示:描述器是怎么拦截并盖过实例属性的

每一步都是本文上面用Python 3.14跑出来的原始输出。

数据描述器的优先级高于实例字典
这一步新验证的结果

    本文用到的工具

    • 本机 Python 3.14.7
    • 自定义 __get__/__set__ 描述器:验证优先级规则
    京ICP备2026067502号-1京公网安备11010502063487号