欢迎光临

Python描述符(Descriptor)协议深度解析:从属性访问机制到property、classmethod与Django模型字段实现原理

什么是描述符协议

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代码与描述符

属性查找的优先级机制

理解描述符的关键在于搞清楚Python的属性查找顺序。当我们访问

1
obj.attr

时,Python并非简单地从实例字典中取值,而是遵循一套精确的查找链:

  1. 数据描述符(定义在类的
    1
    __dict__

    中,且具有

    1
    __set__

    1
    __delete__

  2. 实例字典
    1
    obj.__dict__

  3. 非数据描述符(定义在类的
    1
    __dict__

    中,只有

    1
    __get__

  4. 类字典及继承链

用一个直观的例子来验证:


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方法绑定的全部秘密。

代码编辑器中的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
__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__

最佳实践总结

实践 说明
使用

1
__set_name__
Python 3.6+ 自动获取属性名,无需手动传参
状态存入实例字典 使用

1
setattr(obj, self.storage_name, value)

而非

1
self.value = value
处理

1
obj is None
通过类访问时

1
obj

1
None

,应返回描述符自身

使用

1
__slots__

时预留槽位

确保描述符的存储名称在

1
__slots__

中声明

优先组合而非继承 描述符适合作为可组合的混入组件,而非继承层次

描述符的高级应用模式

惰性计算属性

有些属性计算成本高昂,但一旦计算后值不会变。用描述符可以实现惰性计算——首次访问时计算并缓存,后续访问直接返回缓存值:


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
__getattribute__

的关系

很多开发者对描述符、

1
__getattr__

1
__getattribute__

三者的关系感到困惑。完整的属性查找流程如下:

  1. 调用
    1
    type(obj).__getattribute__(obj, name)
  2. 1
    type(obj).__mro__

    中查找名为

    1
    name

    的类属性

  3. 如果找到的是数据描述符,调用其
    1
    __get__

    并返回

  4. 查找
    1
    obj.__dict__[name]

    ,找到则返回

  5. 查找
    1
    type(obj).__mro__

    中的非数据描述符,找到则调用

    1
    __get__

    返回

  6. 以上都未找到,触发
    1
    type(obj).__getattr__(obj, name)

    (如果定义了的话)

  7. 最终抛出
    1
    AttributeError

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属性系统的底层运作方式。

核心要点回顾:

  • 描述符协议是
    1
    property

    1
    classmethod

    1
    staticmethod

    和方法绑定的底层实现

  • 数据描述符优先级高于实例字典,非数据描述符优先级低于实例字典
  • 状态必须存储在实例字典中,不能存在描述符自身
  • 1
    __set_name__

    是Python 3.6+的推荐方式,自动获取属性名

  • Django模型字段、SQLAlchemy列属性、Pydantic验证器都基于描述符实现
  • 描述符适合实现类型验证、惰性计算、只读属性、变更观察等横切关注点

当你发现自己在一个类中反复编写相似的getter/setter逻辑时,就该考虑用描述符来抽象了。它不是炫技的工具,而是消除重复、提升代码表达力的利器。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » Python描述符(Descriptor)协议深度解析:从属性访问机制到property、classmethod与Django模型字段实现原理
分享到: 更多 (0)