大家好,我是正在实战各种 AI 项目的程序员晚枫。
你有没有想过,obj.age = -1 时是谁在拦截这次赋值、抛出 ValueError?property 的底层到底在做什么?Django ORM 的字段验证又是怎么实现的?
答案只有一个词:描述符(Descriptor)。
描述符是 Python 属性访问的核心机制,也是 property、classmethod、staticmethod、甚至槽(__slots__)的底层实现。理解它,你就真正理解了 Python 的对象模型。
🎯 什么是描述符?
描述符协议
一个对象,只要定义了以下三个方法中的至少一个,就是一个描述符:
| 方法 | 作用 | 触发场景 |
|---|
__get__(self, instance, owner) | 获取属性值 | obj.attr 或 Class.attr |
__set__(self, instance, value) | 设置属性值 | obj.attr = value |
__delete__(self, instance) | 删除属性 | del obj.attr |
还有一个辅助方法:
| 方法 | 作用 | 触发场景 |
|---|
__set_name__(self, owner, name) | 绑定名称 | 类定义时(Python 3.6+) |
数据描述符 vs 非数据描述符
这是理解描述符优先级的关键:
- 数据描述符:同时定义了
__get__ 和 __set__(或 __delete__)——优先级高于实例 __dict__ - 非数据描述符:只定义了
__get__——优先级低于实例 __dict__
1 2 3 4 5 6 7 8 9 10 11 12
| class DataDescriptor: def __get__(self, instance, owner): return instance.__dict__.get('_value', 0) def __set__(self, instance, value): instance.__dict__['_value'] = value
class NonDataDescriptor: def __get__(self, instance, owner): return 42
|
🔑 实战:验证描述符
这是描述符最典型的应用——自动校验属性值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| class Validator: """通用验证描述符基类""" def __set_name__(self, owner, name): self.public_name = name self.private_name = '_' + name def __get__(self, instance, owner): if instance is None: return self return getattr(instance, self.private_name, None) def __set__(self, instance, value): self.validate(value) setattr(instance, self.private_name, value) def validate(self, value): pass
class PositiveNumber(Validator): """验证正数""" def validate(self, value): if not isinstance(value, (int, float)): raise TypeError(f'{self.public_name} 必须是数字,得到 {type(value).__name__}') if value <= 0: raise ValueError(f'{self.public_name} 必须是正数,得到 {value}')
class NonBlankString(Validator): """验证非空字符串""" def validate(self, value): if not isinstance(value, str): raise TypeError(f'{self.public_name} 必须是字符串') if not value.strip(): raise ValueError(f'{self.public_name} 不能为空字符串')
class LineItem: """商品条目(ORM 字段验证典型用例)""" description = NonBlankString() weight = PositiveNumber() price = PositiveNumber() def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price @property def subtotal(self): return self.weight * self.price
item = LineItem('有机苹果', 1.5, 10.0) print(item.description) print(item.subtotal)
item.weight = -1 item.price = 'abc' item.description = ' '
|
🔧 property 的本质
property 其实是一个内置的描述符类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| class Circle: def __init__(self, radius): self._radius = radius @property def radius(self): return self._radius @radius.setter def radius(self, value): if value < 0: raise ValueError('半径不能为负数') self._radius = value @radius.deleter def radius(self): del self._radius
class RadiusDescriptor: def __get__(self, instance, owner): if instance is None: return self return instance._radius def __set__(self, instance, value): if value < 0: raise ValueError('半径不能为负数') instance._radius = value
class Circle2: radius = RadiusDescriptor() def __init__(self, radius): self.radius = radius
|
🔍 属性查找顺序
这是理解描述符的关键。当你访问 obj.attr 时,Python 的查找顺序是:
1 2 3
| 1. type(obj).__mro__ 中找到数据描述符(有 __set__/__delete__)→ 直接调用其 __get__ 2. obj.__dict__ 中的实例属性 3. type(obj).__mro__ 中找到非数据描述符或类属性 → 调用 __get__(如果有)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class DataDesc: def __get__(self, instance, owner): return "data descriptor" def __set__(self, instance, value): pass
class NonDataDesc: def __get__(self, instance, owner): return "non-data descriptor"
class MyClass: data = DataDesc() non_data = NonDataDesc()
obj = MyClass()
obj.__dict__['data'] = 'instance value' print(obj.data)
obj.__dict__['non_data'] = 'instance value' print(obj.non_data)
|
🏗️ 描述符工厂:动态生成描述符
当多个字段需要相似的验证逻辑时,可以用工厂函数简化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| def quantity(storage_name): """数量字段描述符工厂""" def qty_getter(instance): return instance.__dict__[storage_name] def qty_setter(instance, value): if value > 0: instance.__dict__[storage_name] = value else: raise ValueError(f'{storage_name} 必须大于 0') return property(qty_getter, qty_setter)
class LineItem: weight = quantity('weight') price = quantity('price') def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price
|
🗄️ ORM 原理揭秘
Django ORM 字段验证、SQLAlchemy Column 的核心实现,都基于描述符:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| class Field: """模拟 ORM 字段描述符""" def __init__(self, field_type, nullable=True, default=None): self.field_type = field_type self.nullable = nullable self.default = default def __set_name__(self, owner, name): self.name = name self.column_name = name def __get__(self, instance, owner): if instance is None: return self return instance.__dict__.get(self.name, self.default) def __set__(self, instance, value): if value is None and not self.nullable: raise ValueError(f'{self.name} 不能为 None') if value is not None and not isinstance(value, self.field_type): raise TypeError( f'{self.name} 应为 {self.field_type.__name__},' f'得到 {type(value).__name__}' ) instance.__dict__[self.name] = value
class Model: """简单 ORM 基类""" @classmethod def get_fields(cls): return { name: field for name, field in cls.__dict__.items() if isinstance(field, Field) }
class User(Model): name = Field(str, nullable=False) age = Field(int, default=0) email = Field(str) def __init__(self, name, age=0, email=None): self.name = name self.age = age self.email = email def __repr__(self): return f'User(name={self.name!r}, age={self.age}, email={self.email!r})'
user = User('张三', 25, 'zhang@example.com') print(user) print(user.name)
user.name = None user.age = 'abc'
print(User.name) print(User.get_fields())
|
💡 实战技巧
1. 用 __set_name__ 避免重复配置(Python 3.6+)
1 2 3 4 5 6 7
| class OldClass: age = Validator('age', 0, 150)
class NewClass: age = Validator(0, 150)
|
2. 描述符中用实例 __dict__ 存储,避免无限递归
1 2 3 4 5 6 7 8 9 10 11 12
| class Descriptor: def __get__(self, instance, owner): if instance is None: return self return instance.__dict__.get(self.name) def __set__(self, instance, value): instance.__dict__[self.name] = value
|
3. 类访问时返回描述符自身
1 2 3 4
| def __get__(self, instance, owner): if instance is None: return self return instance.__dict__.get(self.name)
|
⚠️ 常见陷阱
1. 忘记处理 instance is None 的情况
1 2 3 4 5 6 7 8 9 10 11
| class BadDescriptor: def __get__(self, instance, owner): return instance._value
class GoodDescriptor: def __get__(self, instance, owner): if instance is None: return self return instance._value
|
2. 描述符必须定义在类中,不能在实例中
1 2 3 4 5 6 7 8 9 10
| class MyDescriptor: def __get__(self, instance, owner): return 42
class MyClass: attr = MyDescriptor()
obj = MyClass() obj.attr2 = MyDescriptor() print(obj.attr2)
|
3. 非数据描述符会被实例属性遮蔽
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Method: def __get__(self, instance, owner): return lambda: "method result"
class MyClass: my_method = Method()
obj = MyClass() print(obj.my_method())
obj.__dict__['my_method'] = 'instance value' print(obj.my_method)
|
🎯 本讲总结
描述符协议:实现 __get__、__set__、__delete__ 的对象,能够拦截属性访问。
数据 vs 非数据描述符:有 __set__ 的数据描述符优先级高于实例 __dict__;只有 __get__ 的非数据描述符会被实例属性覆盖。
属性查找顺序:数据描述符 → 实例 __dict__ → 非数据描述符/类属性。
property 本质:内置的数据描述符类,三个方法(getter/setter/deleter)对应 __get__/__set__/__delete__。
实际应用:属性验证、ORM 字段、缓存属性(functools.cached_property 就是非数据描述符)。
**__set_name__**:Python 3.6+ 新增,让描述符自动感知所属类和属性名,告别硬编码。
📚 推荐教材
《Python 编程从入门到实践(第 3 版)》 | 《流畅的 Python(第 2 版)》 | 《CPython 设计与实现》
学习路线: 零基础 → 《从入门到实践》 → 《流畅的 Python》 → 本门课程 → 《CPython 设计与实现》
🎓 加入《流畅的 Python》直播共读营
学到这里,如果你想系统吃透这本书——欢迎加入我的直播共读课。
- 每周直播精讲,逐章拆解核心知识点
- 专属学习群,随时答疑交流
- 试运营特惠:
499 元 → 299 元
👉 【立即报名《流畅的 Python》共读课】:https://mp.weixin.qq.com/s/ivHJwn1nNx5ug4TFrapvGg
🔗 课程导航
← 上一讲:接口与协议 | 下一讲:动态属性和特性 →
💬 联系我
主营业务:AI 编程培训、企业内训、技术咨询
🎓 AI 编程实战课程
想系统学习 AI 编程?程序员晚枫的 AI 编程实战课 帮你从零上手!
