__set__ 是类型校验的核心入口,必须在赋值时拦截并校验;需用 __set_name__ 绑定属性名以支持复用;不兼容 __slots__ 类时应改用 weakkeydictionary;复合校验应拆分为独立描述符以保持单一职责。

描述符的 __set__ 方法是类型校验的核心入口
类型校验必须发生在赋值时,而描述符正是通过拦截 __set__ 来实现这一点的。它不像属性装饰器(如 @property)只控制单个属性,而是可复用、可组合的通用机制。
关键点在于:__set__ 接收三个参数:self(描述符实例)、instance(宿主对象)、value(待赋的值)。校验逻辑就写在这里,不通过就直接抛出 TypeError。
常见错误是把校验逻辑放在 __init__ 或 __get__ 里——前者只在描述符创建时运行一次,后者在读取时才触发,完全无法阻止非法赋值。
示例:
class Typed:
def __init__(self, expected_type):
self.expected_type = expected_type
def __set__(self, instance, value):
if not isinstance(value, self.expected_type):
raise TypeError(f"Expected {self.expected_type.__name__}, got {type(value).__name__}")
instance.__dict__[self.name] = value
def __set_name__(self, owner, name):
self.name = name
必须用 __set_name__ 绑定属性名,否则无法存取实例数据
描述符本身是类级别的对象,多个实例共享同一个描述符对象。如果不记录宿主类中该属性的真实名称,就无法在 __set__ 和 __get__ 中正确操作对应实例的属性值。
手动硬编码属性名(比如写死 instance._age)会破坏复用性;用 __dict__ 存到实例上又必须知道键名——这个键名只能由 __set_name__ 提供。
容易被忽略的是:Python 3.6+ 才支持 __set_name__,旧版本需退化为在宿主类中显式传入名称(例如 age = Typed(int, "age")),但那样易错且冗余。
使用场景:所有需要将描述符绑定到具体字段名的地方,包括自定义验证、懒加载、缓存代理等。
快速生成专业的 Python 脚本和应用代码。一键创建完整项目结构,支持CLI、API、爬虫、Bot、Django等多种项目类型,包含完整的项目结构、配置文件、依赖管理、测试、README和文档。
描述符不能直接用于 __slots__ 类,除非改用 __set__ 写入类变量或外部映射
如果宿主类定义了 __slots__,实例就没有 __dict__,而上面示例中 instance.__dict__[self.name] = value 就会报 AttributeError。
解决方式有两种:
- 改用
setattr(instance, self.name, value)——但前提是self.name在__slots__中已声明,否则仍失败 - 用弱引用字典(
weakref.WeakKeyDictionary)在描述符内部维护值映射,绕过实例存储限制
性能影响:弱引用字典方案多一层哈希查找,但避免了动态属性开销;对高频访问字段需实测权衡。兼容性上,它能同时适配有无 __slots__ 的类。
复合校验(如非空 + 类型)应封装成独立描述符,而非在 __set__ 里堆逻辑
一个描述符只做一件事。把“必须是字符串”和“不能为空”塞进同一个 __set__,会导致复用困难、测试膨胀、错误信息模糊。
更合理的方式是组合:
-
String描述符负责类型 -
NotNull描述符负责空值检查(可配合__set__前置调用) - 或者用装饰器风格的工厂函数:
NonEmpty(String())
参数差异明显:String 关注 expected_type,NonEmpty 关注 allow_empty 或 strip 行为。混在一起会让构造签名混乱,比如 Typed(str, non_empty=True, strip=True) 已经偏离“类型校验”本意。
真正复杂的业务规则(如邮箱格式、范围限制)建议另起专用描述符,不要试图让 Typed 变成万能校验器。
描述符的威力在于解耦,不是把所有校验逻辑都塞进一个类里。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










