17370845950

如何让类属性变成描述符且支持类型检查
描述符必须实现__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 和异常抛出;其余都是让这行代码能被准确调用的基础设施。