描述符通过__get__和__set__拦截访问,值必须存入实例__dict__(如instance.__dict__[self._attr_name])以避免共享;__set_name__是获取字段名的唯一可靠方式,用于ORM映射;__get__须判instance is None返回自身,__set__需类型校验,且与__slots__冲突需规避。

描述符的 __get__ 和 __set__ 怎么配合实例字典存取值
ORM 字段映射本质是把类定义里的声明(比如 name = StringField())和实例数据(比如 user.name = "Alice")桥接起来。描述符靠 __get__ 和 __set__ 拦截访问,但不能直接在描述符对象里存值——否则所有实例共享同一份数据。必须把值落到实例的 __dict__ 里。
常见错误是写成 self._value = value,结果多个实例互相覆盖。正确做法是用实例作为 key 存到描述符的私有字典中,或更稳妥地:直接操作 instance.__dict__。
- 推荐用
instance.__dict__[self._attr_name]存值,避免触发其他描述符或 property -
_attr_name需在描述符初始化时由上层传入(比如通过__set_name__),不能硬编码 - 读取时优先查
instance.__dict__,不存在再返回默认值(如None或字段定义的default)
为什么必须实现 __set_name__ 才能知道字段名
Python 3.6+ 中,__set_name__ 是描述符自动获知自身绑定属性名的唯一可靠方式。没有它,你就无法生成数据库列名、校验提示字段、或构建查询条件——所有 ORM 行为都依赖这个名称上下文。
不实现 __set_name__ 的后果:你只能手动传字段名(如 name = StringField(db_column="user_name")),既冗余又易错;或者用 inspect 解析源码(不可靠,且不支持动态类生成)。
立即学习“Python免费学习笔记(深入)”;
-
__set_name__(self, owner, name)中的name就是类中写的变量名(如"email") - 务必保存它(如
self.name = name),后续序列化、SQL 构建都靠它 - 若兼容旧版本 Python,需 fallback 到手动传参,但现代项目应直接要求 3.6+
__get__ 返回值类型不一致会导致 ORM 查询失败
描述符的 __get__ 在访问类(User.name)和实例(u.name)时行为不同:类访问应返回描述符本身(用于元编程),实例访问才返回字段值。漏判 instance is None 会导致 User.name 拿到字符串而非 StringField 对象,ORM 的模型分析直接崩掉。
另一个坑是类型污染:比如 IntegerField 的 __set__ 没做类型检查,允许存字符串,后续生成 SQL 时 WHERE id = '123' 可能被 DB 当字符串处理(尤其 SQLite)。
-
__get__开头必须写if instance is None: return self -
__set__中强制转换或抛异常(如int(value)+ValueError)比静默转更安全 - 对可空字段,
None是合法值,但得和“未赋值”区分开——后者应从instance.__dict__里查不到键
描述符 + __slots__ 一起用会出问题
如果模型类用了 __slots__,instance.__dict__ 就不存在,而大多数描述符默认依赖它存值。这时 __set__ 会抛 AttributeError: 'User' object has no attribute '__dict__'。
这不是描述符写错了,而是设计冲突:ORM 描述符需要运行时动态挂字段值,__slots__ 却显式禁止新增属性。二者本质目标相反。
- 要么放弃
__slots__(多数 ORM 如 SQLAlchemy/Django 都不启用) - 要么改用弱引用字典(
weakref.WeakKeyDictionary)存值,但会增加 GC 压力 - 或者在描述符中判断
hasattr(instance, '__dict__'),fallback 到其他存储策略(如线程局部存储——仅限单线程场景)
实际项目里,除非明确压内存且确认无并发问题,否则别在 ORM 模型上加 __slots__。那个省下的几 KB,往往换来调试半天的隐性 bug。


















