实现一个数据描述器拦截属性读写,验证即使直接往实例__dict__里塞同名key,读取依然会被描述器拦截,看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']。
如果直接往实例字典里塞一个同名的键,会发生什么。
结果p.__dict__['x'] = 999 # 直接往实例字典里塞了一个"x" p.x [描述器] 拦截了对 x 的读取 → 100 ← 还是描述器给出的值,不是999!
哪怕往 p.__dict__ 里塞进了一个名叫 x 的键(值是999),读 p.x 依然走的是描述器的 __get__,返回的是描述器自己维护的100,那个999完全被晾在一边没人用。这就是"数据描述器"(同时定义了 __get__ 和 __set__)的优先级规则:类上的数据描述器,会盖过同名的实例属性——这也是Python属性查找里比"先查实例字典"更靠前的一条规则。
@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跑出来的原始输出。
__get__/__set__ 描述器:验证优先级规则