描述符类必须实现__set__和__delete__方法才能支持赋值与删除;仅含__get__为只读描述符。其必须作为类变量实例化(如attr = MyDescriptor()),且__set__需存值到WeakKeyDictionary或实例__dict__中以隔离状态,避免共享或内存泄漏。

描述符类必须实现 __set__ 和 __delete__ 方法
仅定义 __get__ 的类是只读描述符;要支持赋值和删除,__set__(self, obj, value) 和 __delete__(self, obj) 二者缺一不可。Python 在对实例属性赋值时,若发现同名类属性是描述符且实现了 __set__,就会跳过实例字典、直接调用该方法——这是描述符生效的前提。
常见错误是漏写 __set__ 却以为 @property 那套逻辑能复用;描述符协议和 @property 无关,后者只是内置描述符的语法糖,不能直接混用。
-
__set__必须接收三个参数:描述符实例、托管对象(即使用该属性的实例)、待赋的值 -
__delete__必须接收两个参数:描述符实例、托管对象 - 若未实现
__set__,对该属性赋值会直接在实例上新建同名属性,覆盖描述符——此时__get__后续也不再被调用
在类中声明描述符属性时不能加括号
描述符必须作为类变量(class variable)存在,且必须是描述符类的实例,而非类本身。写成 attr = MyDescriptor() 是对的,写成 attr = MyDescriptor(没调用)或 attr = MyDescriptor() 放在 __init__ 里,都会失效。
典型误用场景:想给每个实例配独立描述符,结果把描述符实例化放到了 __init__ 中——这会让它变成普通实例属性,完全不触发描述符协议。
- ✅ 正确:
class MyClass: attr = MyDescriptor() # 类变量,实例化一次 - ❌ 错误:
class MyClass: def __init__(self): self.attr = MyDescriptor() # 实例属性,不触发协议 - ❌ 错误:
class MyClass: attr = MyDescriptor # 没调用,只是类引用
setter/deleter 逻辑需自行管理底层存储位置
描述符本身不自动绑定到实例数据;你得决定值存哪:可以存在描述符实例里(共享)、实例的 __dict__ 里(隔离)、或用 weakref.WeakKeyDictionary 做映射。多数情况推荐后者,避免循环引用和内存泄漏。
如果把值直接存进描述符自己的属性(如 self._value),那所有托管实例将共享这个值——这通常不是想要的行为。
- 推荐方式:用
weakref.WeakKeyDictionary映射实例 → 值,确保实例销毁后自动清理 - 简单替代:存入托管实例的
__dict__,但需约定键名(如_mydescriptor_attr),防止命名冲突 - 不推荐:存在描述符自身属性中,除非明确需要类级别共享状态
注意 __set__ 中对 None 或非法值的处理时机
描述符的 __set__ 是唯一可控的赋值入口,也是做类型校验、范围限制、事件通知的最佳位置。但要注意:如果在 __set__ 中抛异常(如 ValueError),赋值操作会立即失败,不会写入任何地方——这点比 @property 更严格,也更可靠。
容易忽略的是 del obj.attr 触发 __delete__ 后,若后续又访问 obj.attr,__get__ 仍会被调用,但此时底层存储可能已清空。你需要在 __get__ 中判断值是否存在,否则可能抛 AttributeError 或返回意外默认值。
- 在
__set__中校验并规范化输入值(例如转为 int、截断字符串) - 在
__delete__中清除对应存储,不要留空洞状态 - 在
__get__开头检查值是否存在,缺失时明确抛错或返回 sentinel(如NotImplemented),别依赖隐式行为
描述符的 setter/deleter 看似只是多写两个方法,实际难点在于存储策略的选择和生命周期管理——特别是跨实例状态隔离与自动清理,这里出错往往没有明显报错,而是数据静默错乱。










