什么是描述符协议
Python描述符协议是语言中最强大却也最容易被忽视的特性之一。简单来说,任何实现了
1 | __get__ |
、
1 | __set__ |
或
1 | __delete__ |
方法的类,其实例在被用作另一个类的属性时,就会触发这些特殊方法,而不是像普通属性那样直接返回存储的值。这一机制是
1 | property |
、
1 | classmethod |
、
1 | staticmethod |
乃至Django模型字段的底层基石。
描述符协议的三个核心方法签名如下:
1
2
3
4
5
6
7
8
9
10
11
12 class Descriptor:
def __get__(self, obj, objtype=None):
"""访问属性时调用,返回属性值"""
pass
def __set__(self, obj, value):
"""设置属性时调用,无返回值"""
pass
def __delete__(self, obj):
"""删除属性时调用,无返回值"""
pass
根据实现的方法不同,描述符分为两类:
- 数据描述符(Data Descriptor):同时实现了
1__get__
和
1__set__(或
1__delete__)的描述符。数据描述符的优先级高于实例字典。
- 非数据描述符(Non-Data Descriptor):只实现了
1__get__
的描述符。非数据描述符的优先级低于实例字典。
这一优先级差异看似微小,却是Python属性查找顺序的核心,稍后我们会深入探讨。

属性查找的优先级机制
理解描述符的关键在于搞清楚Python的属性查找顺序。当我们访问
1 | obj.attr |
时,Python并非简单地从实例字典中取值,而是遵循一套精确的查找链:
- 数据描述符(定义在类的
1__dict__
中,且具有
1__set__或
1__delete__)
- 实例字典(
1obj.__dict__
)
- 非数据描述符(定义在类的
1__dict__
中,只有
1__get__)
- 类字典及继承链
用一个直观的例子来验证:
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 class DataDesc:
"""数据描述符:同时有 __get__ 和 __set__"""
def __get__(self, obj, objtype=None):
return 'from DataDesc __get__'
def __set__(self, obj, value):
obj.__dict__)['_data_desc_val'] = value
class NonDataDesc:
"""非数据描述符:只有 __get__"""
def __get__(self, obj, objtype=None):
return 'from NonDataDesc __get__'
class MyClass:
data_desc = DataDesc() # 数据描述符
non_data_desc = NonDataDesc() # 非数据描述符
obj = MyClass()
# 场景1: 数据描述符 vs 实例字典
obj.__dict__)['data_desc'] = 'instance dict value'
print(obj.data_desc) # 输出: from DataDesc __get__
# 数据描述符赢了!
# 场景2: 非数据描述符 vs 实例字典
obj.__dict__)['non_data_desc'] = 'instance dict value'
print(obj.non_data_desc) # 输出: instance dict value
# 实例字典赢了!
这个优先级规则是Python面向对象系统中最重要的隐式规则之一。如果你不知道它,就很难理解为什么有时候
1 | obj.x = value |
没有把值存到实例字典里。
我们还可以通过
1 | inspect |
模块来动态判断一个描述符是数据描述符还是非数据描述符:
1
2
3
4 import inspect
print(inspect.isdatadescriptor(DataDesc)) # True
print(inspect.isdatadescriptor(NonDataDesc)) # False
从零实现property装饰器
1 | property |
是Python内置的描述符,也是大多数开发者最早接触的描述符用法。它的本质就是一个数据描述符。让我们从零手写一个,理解其内部机制:
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 class MyProperty:
"""手写 property,理解描述符原理"""
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
# 优先使用 getter 的文档字符串
self.__doc__ = doc or (fget.__doc__ if fget else None)
def __get__(self, obj, objtype=None):
if obj is None:
# 通过类访问时返回描述符自身
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)
def getter(self, fget):
"""支持 @prop.getter 装饰器语法"""
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
"""支持 @prop.setter 装饰器语法"""
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
"""支持 @prop.deleter 装饰器语法"""
return type(self)(self.fget, self.fset, fdel, self.__doc__)
使用方式和内置
1 | 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 class Circle:
def __init__(self, radius):
self._radius = radius
@MyProperty
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
c = Circle(5)
print(c.radius) # 5
c.radius = 10 # 调用 __set__
print(c.radius) # 10
# c.radius = -1 # ValueError: 半径不能为负数
关键点在于
1 | setter |
和
1 | deleter |
方法返回的是新的描述符实例,而不是修改原描述符。这就是为什么
1 | @prop.setter |
装饰器不会覆盖原有getter——每次装饰都创建了一个新的描述符对象。
classmethod与staticmethod的描述符原理
1 | classmethod |
和
1 | staticmethod |
都是非数据描述符。它们的实现揭示了描述符协议在方法绑定中的核心作用。
classmethod的实现
当我们写
1 | class C: @classmethod; def f(cls): pass |
时,
1 | f |
实际上是一个描述符实例。访问
1 | C.f |
或
1 | obj.f |
时,
1 | __get__ |
方法将函数绑定到类对象上:
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 class MyClassMethod:
"""手写 classmethod"""
def __init__(self, func):
self.func = func
self.__doc__ = func.__doc__
def __get__(self, obj, cls=None):
if cls is None:
cls = type(obj)
# 将函数绑定到类上,返回绑定方法
from functools import partial
return partial(self.func, cls)
class Service:
_instance = None
@MyClassMethod
def get_instance(cls):
"""获取单例实例"""
if cls._instance is None:
cls._instance = cls()
return cls._instance
# 两种方式访问,效果完全相同
print(Service.get_instance()) # 通过类访问
obj = Service()
print(obj.get_instance()) # 通过实例访问
staticmethod的实现
1 | staticmethod |
更简单——它只是阻止函数绑定到实例或类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 class MyStaticMethod:
"""手写 staticmethod"""
def __init__(self, func):
self.func = func
self.__doc__ = func.__doc__
def __get__(self, obj, objtype=None):
# 直接返回原函数,不做任何绑定
return self.func
class MathUtils:
@MyStaticMethod
def add(a, b):
return a + b
print(MathUtils.add(1, 2)) # 3
obj = MathUtils()
print(obj.add(3, 4)) # 7
普通方法本质上也是非数据描述符——函数对象自带
1 | __get__ |
方法,访问
1 | obj.method |
时,函数的
1 | __get__ |
返回绑定方法(将
1 | obj |
作为第一个参数
1 | self |
传入)。这就是Python方法绑定的全部秘密。

实战:构建类型验证描述符
描述符最经典的应用场景之一是实现类型验证。与其在每个setter中重复写验证逻辑,不如用描述符统一处理:
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 class TypedField:
"""类型验证描述符"""
def __init__(self, expected_type, name=None, default=None):
self.expected_type = expected_type
self.name = name
self.default = default
def __set_name__(self, owner, name):
"""Python 3.6+ 自动调用,获取属性名"""
self.name = name
self.storage_name = f'_typed_{name}'
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.storage_name, self.default)
def __set__(self, obj, value):
if not isinstance(value, self.expected_type):
raise TypeError(
f'{name} 期望类型 {self.expected_type.__name__}, '
f'实际收到 {type(value).__name__}'
)
setattr(obj, self.storage_name, value)
def __delete__(self, obj):
raise AttributeError(f"不能删除 {self.name}")
class User:
name = TypedField(str)
age = TypedField(int)
email = TypedField(str)
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
# 正常使用
u = User('张三', 25, 'zhangsan@example.com')
print(u.name) # 张三
# 类型错误
try:
u.age = '二十五' # TypeError: age 期望类型 int, 实际收到 str
except TypeError as e:
print(e)
这里用到了
1 | __set_name__ |
这个Python 3.6引入的协议方法。它在类创建时自动被调用,传入属主类和属性名,解决了描述符无法获知自身属性名的历史痛点。在此之前,描述符必须在
1 | __init__ |
中手动传入
1 | name |
参数。
进阶:支持多种验证规则
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80 from functools import partial
class ValidatedField:
"""支持多种验证规则的描述符"""
def __init__(self, *validators, name=None, default=None):
self.validators = validators
self.name = name
self.default = default
def __set_name__(self, owner, name):
self.name = name
self.storage_name = f'_validated_{name}'
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.storage_name, self.default)
def __set__(self, obj, value):
for validator in self.validators:
validator(self.name, value)
setattr(obj, self.storage_name, value)
# 验证器函数
def type_check(expected_type):
def validator(name, value):
if not isinstance(value, expected_type):
raise TypeError(f'{name} 期望 {expected_type.__name__}')
return validator
def min_value(min_val):
def validator(name, value):
if value < min_val:
raise ValueError(f'{name} 不能小于 {min_val}')
return validator
def max_length(max_len):
def validator(name, value):
if len(value) > max_len:
raise ValueError(f'{name} 长度不能超过 {max_len}')
return validator
def regex_match(pattern):
import re
compiled = re.compile(pattern)
def validator(name, value):
if not compiled.match(value):
raise ValueError(f'{name} 格式不正确')
return validator
class Product:
name = ValidatedField(
type_check(str),
max_length(100)
)
price = ValidatedField(
type_check((int, float)),
min_value(0)
)
sku = ValidatedField(
type_check(str),
regex_match(r'^[A-Z]{2}\d{4}$')
)
def __init__(self, name, price, sku):
self.name = name
self.price = price
self.sku = sku
p = Product('MacBook Pro', 12999, 'AB1234')
print(f'{p.name}: ¥{p.price} ({p.sku})')
try:
p.sku = 'invalid' # ValueError: sku 格式不正确
except ValueError as e:
print(e)
Django模型字段的描述符实现
Django的模型字段是描述符最经典的生产级应用。当我们写
1 | name = models.CharField(max_length=100) |
时,
1 | CharField |
的实例就是一个描述符,它拦截属性的读写操作,实现数据库值的读写与Python对象之间的转换。
下面我们模拟一个简化版的Django ORM字段系统:
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132 class Field:
"""模拟 Django 模型字段的基类"""
# 类级别字段计数器,确保列顺序
_creation_counter = 0
def __init__(self, db_column=None, null=False, default=None):
self.db_column = db_column
self.null = null
self.default = default
self.creation_counter = Field._creation_counter
Field._creation_counter += 1
def __set_name__(self, owner, name):
self.name = name
self.storage_name = f'_field_{name}'
if self.db_column is None:
self.db_column = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
value = getattr(obj, self.storage_name, None)
if value is None and not self.null:
raise ValueError(f'{self.name} 不允许为空')
return value
def __set__(self, obj, value):
self.validate(value)
setattr(obj, self.storage_name, value)
def validate(self, value):
"""子类覆写此方法实现自定义验证"""
if value is None and not self.null:
raise ValueError(f'{self.name} 不允许为 None')
def to_db(self, value):
"""Python 对象 → 数据库值"""
return value
def from_db(self, value):
"""数据库值 → Python 对象"""
return value
class CharField(Field):
def __init__(self, max_length=255, **kwargs):
super().__init__(**kwargs)
self.max_length = max_length
def validate(self, value):
super().validate(value)
if value is not None and len(str(value)) > self.max_length:
raise ValueError(
f'{self.name} 长度不能超过 {self.max_length}'
)
def to_db(self, value):
return str(value) if value is not None else None
class IntegerField(Field):
def validate(self, value):
super().validate(value)
if value is not None and not isinstance(value, int):
raise TypeError(f'{self.name} 必须是整数')
class BooleanField(Field):
def from_db(self, value):
# 数据库中的 0/1 → Python 的 False/True
return bool(value) if value is not None else None
def to_db(self, value):
# Python 的 True/False → 数据库的 1/0
return int(value) if value is not None else None
class ModelMeta(type):
"""元类:收集字段信息,构建表结构"""
def __new__(mcs, name, bases, namespace):
fields = {}
for key, value in namespace.items():
if isinstance(value, Field):
fields[key] = value
namespace['_fields'] = fields
namespace['_table_name'] = namespace.get('__tablename__',
name.lower())
cls = super().__new__(mcs, name, bases, namespace)
return cls
class Model(metaclass=ModelMeta):
"""简化版 Django Model 基类"""
def __init__(self, **kwargs):
for name, field in self._fields.items():
value = kwargs.get(name, field.default)
setattr(self, name, value)
def save(self):
"""模拟保存到数据库"""
columns = []
values = []
for name, field in self._fields.items():
value = getattr(self, name)
columns.append(field.db_column)
values.append(field.to_db(value))
sql = f"INSERT INTO {self._table_name} ({', '.join(columns)}) " f"VALUES ({', '.join(['?' for _ in values])})"
print(f"[SQL] {sql}")
print(f"[VALUES] {values}")
return sql, values
class Article(Model):
__tablename__ = 'articles'
title = CharField(max_length=200, db_column='title')
views = IntegerField(db_column='view_count', default=0)
published = BooleanField(db_column='is_published', default=False)
# 使用
art = Article(title='描述符深度解析', views=1024, published=True)
print(f'标题: {art.title}')
print(f'阅读: {art.views}')
art.save()
# [SQL] INSERT INTO articles (title, view_count, is_published) VALUES (?, ?, ?)
# [VALUES] ['描述符深度解析', 1024, 1]
注意
1 | BooleanField.to_db |
将
1 | True |
转为
1 | 1 |
,而
1 | from_db |
将
1 | 1 |
转回
1 | True |
——这正是描述符在属性访问层面实现ORM转换的核心价值。使用者完全不需要关心数据库存储格式,读写属性就是读写Python对象。

描述符的坑与最佳实践
坑1:描述符是类级别共享的
描述符实例定义在类体中,所有该类的实例共享同一个描述符对象。这意味着描述符中不能存储实例特定的状态——状态必须存在实例的
1 | __dict__ |
中。
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 # 错误示范:在描述符自身存储状态
class BuggyDescriptor:
def __init__(self):
self.value = None # 所有实例共享同一个 value!
def __get__(self, obj, objtype=None):
return self.value
def __set__(self, obj, value):
self.value = value
class Foo:
attr = BuggyDescriptor()
a, b = Foo(), Foo()
a.attr = 'hello'
print(b.attr) # hello —— 串了!
# 正确做法:将值存到实例字典中
class FixedDescriptor:
def __set_name__(self, owner, name):
self.storage = f'_desc_{name}'
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.storage, None)
def __set__(self, obj, value):
setattr(obj, self.storage, value)
class Bar:
attr = FixedDescriptor()
a, b = Bar(), Bar()
a.attr = 'hello'
print(b.attr) # None —— 各实例独立
坑2:
1
__slots__
1 | __slots__ |
与描述符的交互
如果类使用了
1 | __slots__ |
,那么实例没有
1 | __dict__ |
,描述符就无法通过
1 | setattr |
存储值。解决方案是在
1 | __slots__ |
中预留存储槽位:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 class SlotDescriptor:
def __set_name__(self, owner, name):
self.storage = f'_slot_{name}'
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.storage, None)
def __set__(self, obj, value):
setattr(obj, self.storage, value)
class SlottedClass:
__slots__ = ('_slot_x', '_slot_y')
x = SlotDescriptor()
y = SlotDescriptor()
obj = SlottedClass()
obj.x = 42
print(obj.x) # 42
坑3:描述符只在类体中定义时生效
描述符必须定义在类体中(作为类属性),直接赋值给实例不会触发描述符协议:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 class MyDesc:
def __get__(self, obj, objtype=None):
return 'from descriptor'
# 正确:在类体中定义
class Good:
attr = MyDesc()
obj = Good()
print(obj.attr) # from descriptor
# 错误:直接赋值给实例
class Bad:
pass
b = Bad()
b.attr = MyDesc() # 只是把描述符实例存入了实例字典
print(b.attr) # <MyDesc object at 0x...>,不会触发 __get__
最佳实践总结
| 实践 | 说明 | ||||||
|---|---|---|---|---|---|---|---|
使用
|
Python 3.6+ 自动获取属性名,无需手动传参 | ||||||
| 状态存入实例字典 | 使用
而非
|
||||||
处理
|
通过类访问时
为
,应返回描述符自身 |
||||||
使用
时预留槽位 |
确保描述符的存储名称在
中声明 |
||||||
| 优先组合而非继承 | 描述符适合作为可组合的混入组件,而非继承层次 |
描述符的高级应用模式
惰性计算属性
有些属性计算成本高昂,但一旦计算后值不会变。用描述符可以实现惰性计算——首次访问时计算并缓存,后续访问直接返回缓存值:
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 class LazyProperty:
"""惰性计算描述符,只计算一次"""
def __init__(self, func):
self.func = func
self.__doc__ = func.__doc__
def __set_name__(self, owner, name):
self.storage_name = f'_lazy_{name}'
def __get__(self, obj, objtype=None):
if obj is None:
return self
# 检查是否已缓存
if hasattr(obj, self.storage_name):
return getattr(obj, self.storage_name)
# 首次访问,计算并缓存
value = self.func(obj)
setattr(obj, self.storage_name, value)
return value
class DataProcessor:
def __init__(self, data_path):
self.data_path = data_path
@LazyProperty
def data(self):
"""加载大数据集(只加载一次)"""
print(f'正在加载 {self.data_path} ...')
import time
time.sleep(2) # 模拟耗时加载
return list(range(1000000))
@LazyProperty
def statistics(self):
"""计算统计信息(依赖 data,只计算一次)"""
print('正在计算统计信息...')
d = self.data # 此处会触发 data 的惰性加载
return {
'count': len(d),
'sum': sum(d),
'avg': sum(d) / len(d)
}
processor = DataProcessor('/data/large_dataset.csv')
print('对象已创建,数据尚未加载')
print(f'统计信息: {processor.statistics}') # 首次访问才加载
print(f'再次访问: {processor.statistics}') # 直接返回缓存
只读属性描述符
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 class ReadOnly:
"""只读属性描述符,初始化后不可修改"""
def __set_name__(self, owner, name):
self.storage = f'_readonly_{name}'
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.storage, None)
def __set__(self, obj, value):
if hasattr(obj, self.storage):
raise AttributeError('只读属性不可修改')
setattr(obj, self.storage, value)
class Config:
db_host = ReadOnly()
db_port = ReadOnly()
def __init__(self, host, port):
self.db_host = host
self.db_port = port
cfg = Config('localhost', 5432)
print(cfg.db_host) # localhost
try:
cfg.db_host = '127.0.0.1' # AttributeError: 只读属性不可修改
except AttributeError as e:
print(e)
属性变更观察者
描述符可以用来实现属性变更通知机制,这在响应式编程和事件驱动架构中非常有用:
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 class Observable:
"""可观察属性描述符"""
def __set_name__(self, owner, name):
self.storage = f'_observable_{name}'
self.name = name
def __init__(self, *callbacks):
self.callbacks = list(callbacks)
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.storage, None)
def __set__(self, obj, value):
old_value = getattr(obj, self.storage, None)
setattr(obj, self.storage, value)
if old_value != value:
for callback in self.callbacks:
callback(obj, self.name, old_value, value)
def log_change(obj, name, old, new):
print(f'[LOG] {obj.__class__.__name__}.{name}: {old} → {new}')
def validate_range(obj, name, old, new):
if isinstance(new, (int, float)) and new < 0:
raise ValueError(f'{name} 不能为负数')
class Sensor:
temperature = Observable(log_change, validate_range)
humidity = Observable(log_change)
def __init__(self, temp, humidity):
self.temperature = temp
self.humidity = humidity
sensor = Sensor(25.0, 60)
# [LOG] Sensor.temperature: None → 25.0
# [LOG] Sensor.humidity: None → 60
sensor.temperature = 28.5
# [LOG] Sensor.temperature: 25.0 → 28.5
sensor.humidity = 65
# [LOG] Sensor.humidity: 60 → 65
描述符与
1
__getattr__
1 | __getattr__ |
、
1 | __getattribute__ |
的关系
很多开发者对描述符、
1 | __getattr__ |
和
1 | __getattribute__ |
三者的关系感到困惑。完整的属性查找流程如下:
- 调用
1type(obj).__getattribute__(obj, name)
- 在
1type(obj).__mro__
中查找名为
1name的类属性
- 如果找到的是数据描述符,调用其
1__get__
并返回
- 查找
1obj.__dict__[name]
,找到则返回
- 查找
1type(obj).__mro__
中的非数据描述符,找到则调用
1__get__返回
- 以上都未找到,触发
1type(obj).__getattr__(obj, name)
(如果定义了的话)
- 最终抛出
1AttributeError
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 class FullDemo:
"""展示完整查找链"""
class DataDesc:
def __get__(self, obj, objtype=None):
return 'data descriptor'
def __set__(self, obj, value):
pass
class NonDataDesc:
def __get__(self, obj, objtype=None):
return 'non-data descriptor'
data_desc = DataDesc()
non_data_desc = NonDataDesc()
def __getattr__(self, name):
return f'__getattr__ 处理了 {name}'
obj = FullDemo()
# 数据描述符 > 实例字典
obj.__dict__['data_desc'] = 'instance value'
print(obj.data_desc) # data descriptor
# 实例字典 > 非数据描述符
obj.__dict__['non_data_desc'] = 'instance value'
print(obj.non_data_desc) # instance value
# 什么都没有 → __getattr__
print(obj.unknown_attr) # __getattr__ 处理了 unknown_attr
理解这套查找链是掌握Python面向对象高级特性的关键。它解释了为什么有些属性访问行为不符合直觉——背后都是描述符协议在起作用。
总结
Python描述符协议是语言中最优雅的设计之一。它用极简的三个方法(
1 | __get__ |
、
1 | __set__ |
、
1 | __delete__ |
)统一了属性访问、方法绑定、类型验证、ORM字段映射等看似毫不相干的功能。理解描述符,就是理解了Python属性系统的底层运作方式。
核心要点回顾:
- 描述符协议是
1property
、
1classmethod、
1staticmethod和方法绑定的底层实现
- 数据描述符优先级高于实例字典,非数据描述符优先级低于实例字典
- 状态必须存储在实例字典中,不能存在描述符自身
-
1__set_name__
是Python 3.6+的推荐方式,自动获取属性名
- Django模型字段、SQLAlchemy列属性、Pydantic验证器都基于描述符实现
- 描述符适合实现类型验证、惰性计算、只读属性、变更观察等横切关注点
当你发现自己在一个类中反复编写相似的getter/setter逻辑时,就该考虑用描述符来抽象了。它不是炫技的工具,而是消除重复、提升代码表达力的利器。
汤不热吧