大家好,我是正在实战各种 AI 项目的程序员晚枫。

你有没有想过,obj.age = -1 时是谁在拦截这次赋值、抛出 ValueError?property 的底层到底在做什么?Django ORM 的字段验证又是怎么实现的?

答案只有一个词:描述符(Descriptor)

描述符是 Python 属性访问的核心机制,也是 propertyclassmethodstaticmethod、甚至槽(__slots__)的底层实现。理解它,你就真正理解了 Python 的对象模型。


🎯 什么是描述符?

描述符协议

一个对象,只要定义了以下三个方法中的至少一个,就是一个描述符:

方法作用触发场景
__get__(self, instance, owner)获取属性值obj.attrClass.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
# 数据描述符(有 __set__)
class DataDescriptor:
def __get__(self, instance, owner):
return instance.__dict__.get('_value', 0)

def __set__(self, instance, value):
instance.__dict__['_value'] = value

# 非数据描述符(只有 __get__)
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):
# Python 3.6+,类定义时自动调用
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 # 触发 NonBlankString.__set__
self.weight = weight # 触发 PositiveNumber.__set__
self.price = price

@property
def subtotal(self):
return self.weight * self.price


# 测试
item = LineItem('有机苹果', 1.5, 10.0)
print(item.description) # 有机苹果
print(item.subtotal) # 15.0

# 验证生效
item.weight = -1 # ValueError: weight 必须是正数,得到 -1
item.price = 'abc' # TypeError: price 必须是数字,得到 str
item.description = ' ' # ValueError: 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


# 等价的描述符写法(理解 property 的本质)
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() # 和 @property 效果完全相同

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()

# 数据描述符优先于实例 __dict__
obj.__dict__['data'] = 'instance value'
print(obj.data) # "data descriptor" ← 描述符胜出!

# 非数据描述符被实例 __dict__ 覆盖
obj.__dict__['non_data'] = 'instance value'
print(obj.non_data) # "instance value" ← 实例属性胜出!

🏗️ 描述符工厂:动态生成描述符

当多个字段需要相似的验证逻辑时,可以用工厂函数简化:

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) # User(name='张三', age=25, email='zhang@example.com')
print(user.name) # 张三

user.name = None # ValueError: name 不能为 None
user.age = 'abc' # TypeError: age 应为 int,得到 str

# 通过类访问字段对象
print(User.name) # <Field object> ← __get__ 返回 self(instance is None)
print(User.get_fields()) # {'name': ..., 'age': ..., 'email': ...}

💡 实战技巧

1. 用 __set_name__ 避免重复配置(Python 3.6+)

1
2
3
4
5
6
7
# 旧写法:需要手动指定名称(容易出错)
class OldClass:
age = Validator('age', 0, 150) # 必须重复写字段名

# 新写法:__set_name__ 自动注入
class NewClass:
age = Validator(0, 150) # 描述符自动知道自己叫 'age'

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
# ✅ 直接操作 __dict__,不会触发描述符本身
return instance.__dict__.get(self.name)

def __set__(self, instance, value):
# ✅ 正确
instance.__dict__[self.name] = value
# ❌ 错误:会触发自身的 __set__,造成无限递归
# setattr(instance, 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
# 错误:通过类访问时会报 AttributeError
class BadDescriptor:
def __get__(self, instance, owner):
return instance._value # instance 可能是 None!

# 正确
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) # 直接返回描述符对象,而不是调用 __get__

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()) # "method result"

# 但一旦设置同名实例属性:
obj.__dict__['my_method'] = 'instance value'
print(obj.my_method) # "instance value" ← 非数据描述符被覆盖了!

🎯 本讲总结

描述符协议:实现 __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

🔗 课程导航

上一讲:接口与协议 | 下一讲:动态属性和特性


💬 联系我

平台账号/链接
微信扫码加好友
B 站Python 自动化办公社区

主营业务:AI 编程培训、企业内训、技术咨询

🎓 AI 编程实战课程

想系统学习 AI 编程?程序员晚枫的 AI 编程实战课 帮你从零上手!

fluent-python.png