描述符必须实现__get__、__set__或__delete__之一才能触发协议;只读需__set__抛AttributeError;类型检查须在__set__中用isinstance手动校验,注解无效。

描述符类必须实现 __get__、__set__ 或 __delete__
只定义空方法不够,Python 仅当类中至少实现了这三个特殊方法之一时,才在属性访问时触发描述符协议。如果漏掉 __set__ 却尝试赋值,会回退到实例字典,导致类型检查失效。
常见错误是写了个带 __get__ 的“只读描述符”,但没拦住 obj.attr = 123 —— 因为缺少 __set__,Python 直接把值塞进 obj.__dict__,绕过了所有逻辑。
- 支持读写:必须同时实现
__get__和__set__ - 若只读:
__set__应抛出AttributeError - 避免在描述符里存实例状态(如用
self._value),否则多个实例共享同一值
类型检查靠 __set__ 中的 isinstance() 或 type() 判断
运行时类型校验不是靠注解自动生效的,必须手动写判断逻辑。PEP 484 的类型提示(如 str)在运行时不参与检查,仅用于静态分析工具(mypy)或 IDE 提示。
实际校验建议用 isinstance(value, expected_type),而不是 type(value) is expected_type,前者支持继承关系,后者不支持。
- 支持联合类型(如
int | str):用isinstance(value, (int, str)) - 对泛型(如
list[int])需额外检查内容,isinstance无法识别,得手动遍历 - 若允许
None,记得在类型元组里显式加上type(None)或用Optional
在类中声明描述符属性时,不能用 __annotations__ 替代运行时校验
即使你写了 name: str,它只是存进 __annotations__ 字典,不会自动绑定到描述符行为。必须把类型信息传给描述符实例,例如通过构造参数:
class TypedDescriptor:
def __init__(self, expected_type):
self.expected_type = expected_type
class Person:
name = TypedDescriptor(str) # ← 类型信息在这里传入
否则每个描述符实例不知道该校验什么类型,就只能写死或报错。
- 别试图从
__annotations__动态读取类型来初始化描述符——类体执行时__annotations__还未完全就绪,且难以匹配字段名与描述符实例 - 若想统一管理类型,可配合
__set_name__钩子 + 类变量约定,但复杂度陡增,小项目不推荐 - 使用
dataclasses.field()或pydantic.Field()是更稳妥的替代方案,它们内部已封装了描述符+类型校验
描述符 + 类型检查容易忽略的坑:继承和 __set_name__
当描述符被继承时,父类定义的描述符对象会被所有子类共享。如果你在 __set__ 中缓存了值(比如用 instance.__dict__[self.name] = value),那没问题;但如果误用 self._cache = value,就会跨实例污染。
__set_name__ 是 Python 3.6+ 提供的钩子,会在描述符被赋值给类属性时自动调用,传入类和属性名。它是安全设置 self.name 的唯一可靠时机,比硬编码字符串或依赖 __annotations__ 稳定得多。
- 必须实现
__set_name__(self, owner, name)并保存self.name = name,否则无法在实例字典中正确隔离值 - 不要在
__init__里猜属性名,名字可能被重命名或装饰器修改 - 若描述符用于类变量(非实例变量),需另作设计,标准描述符协议默认面向实例访问
类型检查真正起作用的地方,永远是你写在 set 里的那几行 isinstance 和异常抛出;其余都是让这行代码能被准确调用的基础设施。










