从属性访问到框架魔法:深入理解 Python 描述符协议

从属性访问到框架魔法:深入理解 Python 描述符协议

在 Python 的世界里,有些机制看起来平平无奇,却支撑着许多高级特性。比如你每天都在写的 obj.name,表面上只是一次属性访问,背后却可能经历了一套精妙的查找、拦截、计算与绑定过程。

如果说装饰器让函数拥有了"变身能力",元类让类拥有了"出生控制权",那么描述符协议就是 Python 对象模型里最容易被低估、却最值得深入学习的机制之一。

很多初学者第一次听到"描述符"时会觉得抽象,但你其实早已在使用它:property 是描述符,实例方法是描述符,staticmethodclassmethod 也是描述符,许多 ORM 框架中的字段定义、数据校验库中的属性管理、Web 框架中的延迟加载,都离不开它。

本文将从基础概念讲起,逐步拆解描述符协议的运行规则,并通过实战案例展示它在工程中的价值。


一、什么是描述符协议?

描述符协议,英文是 Descriptor Protocol。简单来说:

只要一个对象定义了 __get____set____delete__ 中的任意一个方法,它就是描述符对象。

这三个方法分别用于控制属性的读取、赋值和删除:

python 复制代码
class Descriptor:
    def __get__(self, instance, owner):
        print("读取属性")

    def __set__(self, instance, value):
        print("设置属性")

    def __delete__(self, instance):
        print("删除属性")

当一个描述符对象被放到类属性中时,它就可以拦截实例对该属性的访问。

python 复制代码
class NameDescriptor:
    def __get__(self, instance, owner):
        return "Python 描述符"

class User:
    name = NameDescriptor()

u = User()
print(u.name)  # Python 描述符

这里的关键点是:name 并不是普通字符串,而是一个实现了 __get__ 方法的对象。当执行 u.name 时,Python 会发现 User.name 是描述符,于是调用它的 __get__ 方法。

这也是描述符最核心的能力:它可以参与并控制属性访问过程。


二、为什么需要描述符?

在日常开发中,我们经常会遇到这些需求:

  1. 属性读取时自动计算;
  2. 属性赋值时做类型检查;
  3. 属性赋值时做范围校验;
  4. 属性访问时记录日志;
  5. 属性删除时执行清理逻辑;
  6. 多个类复用同一套属性管理规则。

如果只是偶尔处理一个属性,property 已经足够。但如果你有很多字段都需要校验,例如用户年龄、商品价格、文章标题、数据库字段类型,那么反复写 @property 和 setter 就会变得冗余。

描述符的价值就在这里:它可以把属性管理逻辑封装成可复用组件。


三、描述符的三个核心方法

1. __get__(self, instance, owner)

当读取属性时触发。

python 复制代码
class DemoDescriptor:
    def __get__(self, instance, owner):
        print("调用 __get__")
        print("instance:", instance)
        print("owner:", owner)
        return "hello"

class Demo:
    value = DemoDescriptor()

d = Demo()
print(d.value)

输出大致如下:

python 复制代码
调用 __get__
instance: <__main__.Demo object at 0x...>
owner: <class '__main__.Demo'>
hello

其中:

  • self 是描述符对象本身;
  • instance 是访问属性的实例,比如 d
  • owner 是实例所属的类,比如 Demo

如果通过类访问属性:

python 复制代码
print(Demo.value)

此时 instance 会是 NoneownerDemo

这在实现框架功能时很有用。例如 ORM 中,类访问字段时可能返回字段对象,实例访问字段时返回字段值。


2. __set__(self, instance, value)

当给属性赋值时触发。

python 复制代码
class PositiveNumber:
    def __set__(self, instance, value):
        if value <= 0:
            raise ValueError("必须是正数")
        instance.__dict__["number"] = value

    def __get__(self, instance, owner):
        return instance.__dict__.get("number")

class Product:
    price = PositiveNumber()

p = Product()
p.price = 99
print(p.price)

p.price = -10  # ValueError: 必须是正数

这里 PositiveNumber 拦截了 price 的赋值过程,使我们可以在写入前进行校验。


3. __delete__(self, instance)

当使用 del obj.attr 删除属性时触发。

python 复制代码
class ProtectedAttribute:
    def __get__(self, instance, owner):
        return instance.__dict__.get("token")

    def __set__(self, instance, value):
        instance.__dict__["token"] = value

    def __delete__(self, instance):
        raise AttributeError("该属性不允许删除")

class Account:
    token = ProtectedAttribute()

a = Account()
a.token = "abc123"
print(a.token)

del a.token  # AttributeError: 该属性不允许删除

这种机制适合保护关键属性,比如认证令牌、配置项或只读字段。


四、数据描述符与非数据描述符

理解描述符时,最重要的一道分水岭是:数据描述符和非数据描述符。

数据描述符

同时定义了 __get____set__,或者定义了 __delete__ 的描述符,通常称为数据描述符。

python 复制代码
class DataDescriptor:
    def __get__(self, instance, owner):
        return "data descriptor"

    def __set__(self, instance, value):
        print("set value")

非数据描述符

只定义 __get__,没有定义 __set____delete__ 的描述符,称为非数据描述符。

python 复制代码
class NonDataDescriptor:
    def __get__(self, instance, owner):
        return "non-data descriptor"

它们最大的区别在于属性查找优先级。


五、属性查找优先级

当执行:

python 复制代码
obj.attr

Python 大致会按照以下顺序查找:

text 复制代码
数据描述符
    ↓
实例字典 obj.__dict__
    ↓
非数据描述符
    ↓
类属性
    ↓
__getattr__

这个顺序非常关键。

看一个例子:

python 复制代码
class DataDesc:
    def __get__(self, instance, owner):
        return "来自数据描述符"

    def __set__(self, instance, value):
        print("写入被数据描述符拦截")

class NonDataDesc:
    def __get__(self, instance, owner):
        return "来自非数据描述符"

class Demo:
    a = DataDesc()
    b = NonDataDesc()

d = Demo()
d.__dict__["a"] = "实例字典中的 a"
d.__dict__["b"] = "实例字典中的 b"

print(d.a)
print(d.b)

输出:

python 复制代码
来自数据描述符
实例字典中的 b

为什么?

因为数据描述符优先级高于实例字典,所以 d.a 会调用 DataDesc.__get__。而非数据描述符优先级低于实例字典,所以 d.b 会被实例字典中的值覆盖。

这也解释了为什么 property 通常很强势:它是数据描述符。


六、手写一个类型校验描述符

下面我们实现一个更实用的描述符:自动校验字段类型。

python 复制代码
class TypedField:
    def __init__(self, field_type):
        self.field_type = field_type
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if not isinstance(value, self.field_type):
            raise TypeError(
                f"{self.name} 必须是 {self.field_type.__name__} 类型"
            )
        instance.__dict__[self.name] = value

这里多了一个新方法:__set_name__

它会在类创建时自动调用,帮助描述符知道自己被赋给了哪个属性名。

使用方式如下:

python 复制代码
class User:
    name = TypedField(str)
    age = TypedField(int)

    def __init__(self, name, age):
        self.name = name
        self.age = age

u = User("Alice", 18)
print(u.name)
print(u.age)

u.age = "18"  # TypeError: age 必须是 int 类型

这个小工具已经具备了很多框架字段系统的雏形。


七、加入范围校验:让描述符更像真实项目

我们继续扩展,让字段不仅能校验类型,还能校验范围。

python 复制代码
class IntegerRange:
    def __init__(self, min_value=None, max_value=None):
        self.min_value = min_value
        self.max_value = max_value
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise TypeError(f"{self.name} 必须是整数")

        if self.min_value is not None and value < self.min_value:
            raise ValueError(f"{self.name} 不能小于 {self.min_value}")

        if self.max_value is not None and value > self.max_value:
            raise ValueError(f"{self.name} 不能大于 {self.max_value}")

        instance.__dict__[self.name] = value

使用它:

python 复制代码
class Student:
    age = IntegerRange(0, 150)
    score = IntegerRange(0, 100)

    def __init__(self, age, score):
        self.age = age
        self.score = score

s = Student(20, 95)
print(s.age, s.score)

s.score = 120  # ValueError: score 不能大于 100

在业务系统中,这种模式非常常见。例如:

  • 用户年龄必须在合理范围内;
  • 商品库存不能为负数;
  • 考试分数必须在 0 到 100;
  • 订单数量必须是正整数。

通过描述符,我们可以把这些规则封装起来,避免散落在业务代码中。


八、描述符与 property 的关系

property 本身就是基于描述符协议实现的。

我们平时这样写:

python 复制代码
class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return 3.14159 * self.radius ** 2

c = Circle(10)
print(c.area)

看起来 area 像普通属性,实际上它是一个描述符。当访问 c.area 时,会调用 property 对象内部的 __get__ 方法。

我们可以用描述符模拟一个简单版 property

python 复制代码
class MyProperty:
    def __init__(self, getter):
        self.getter = getter

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.getter(instance)

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @MyProperty
    def area(self):
        return 3.14159 * self.radius ** 2

c = Circle(5)
print(c.area)

这个例子说明:描述符不是遥远的高级魔法,它就在我们熟悉的语法背后。


九、为什么实例方法会自动绑定 self?

很多 Python 初学者都会问:为什么调用 obj.method() 时,self 会自动传进去?

答案也和描述符有关。

函数对象本身实现了描述符协议。当函数作为类属性存在时,通过实例访问它,会触发函数对象的 __get__,返回一个绑定方法。

python 复制代码
class User:
    def say_hello(self):
        print("hello")

u = User()

print(User.say_hello)
print(u.say_hello)

输出类似:

python 复制代码
<function User.say_hello at 0x...>
<bound method User.say_hello of <__main__.User object at 0x...>>

User.say_hello 是普通函数,而 u.say_hello 是绑定方法。绑定方法已经把 u 绑定给了 self,所以你调用时不需要手动传入。

从这个角度看,描述符协议解释了 Python 面向对象编程里一个非常核心的行为。


十、描述符的结构示意图

下面用一个简单示意图说明描述符参与属性访问的过程:
#mermaid-svg-lW356GzcnvviyyFt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lW356GzcnvviyyFt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lW356GzcnvviyyFt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lW356GzcnvviyyFt .error-icon{fill:#552222;}#mermaid-svg-lW356GzcnvviyyFt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lW356GzcnvviyyFt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lW356GzcnvviyyFt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lW356GzcnvviyyFt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lW356GzcnvviyyFt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lW356GzcnvviyyFt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lW356GzcnvviyyFt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lW356GzcnvviyyFt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lW356GzcnvviyyFt .marker.cross{stroke:#333333;}#mermaid-svg-lW356GzcnvviyyFt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lW356GzcnvviyyFt p{margin:0;}#mermaid-svg-lW356GzcnvviyyFt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lW356GzcnvviyyFt .cluster-label text{fill:#333;}#mermaid-svg-lW356GzcnvviyyFt .cluster-label span{color:#333;}#mermaid-svg-lW356GzcnvviyyFt .cluster-label span p{background-color:transparent;}#mermaid-svg-lW356GzcnvviyyFt .label text,#mermaid-svg-lW356GzcnvviyyFt span{fill:#333;color:#333;}#mermaid-svg-lW356GzcnvviyyFt .node rect,#mermaid-svg-lW356GzcnvviyyFt .node circle,#mermaid-svg-lW356GzcnvviyyFt .node ellipse,#mermaid-svg-lW356GzcnvviyyFt .node polygon,#mermaid-svg-lW356GzcnvviyyFt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lW356GzcnvviyyFt .rough-node .label text,#mermaid-svg-lW356GzcnvviyyFt .node .label text,#mermaid-svg-lW356GzcnvviyyFt .image-shape .label,#mermaid-svg-lW356GzcnvviyyFt .icon-shape .label{text-anchor:middle;}#mermaid-svg-lW356GzcnvviyyFt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lW356GzcnvviyyFt .rough-node .label,#mermaid-svg-lW356GzcnvviyyFt .node .label,#mermaid-svg-lW356GzcnvviyyFt .image-shape .label,#mermaid-svg-lW356GzcnvviyyFt .icon-shape .label{text-align:center;}#mermaid-svg-lW356GzcnvviyyFt .node.clickable{cursor:pointer;}#mermaid-svg-lW356GzcnvviyyFt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lW356GzcnvviyyFt .arrowheadPath{fill:#333333;}#mermaid-svg-lW356GzcnvviyyFt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lW356GzcnvviyyFt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lW356GzcnvviyyFt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lW356GzcnvviyyFt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lW356GzcnvviyyFt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lW356GzcnvviyyFt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lW356GzcnvviyyFt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lW356GzcnvviyyFt .cluster text{fill:#333;}#mermaid-svg-lW356GzcnvviyyFt .cluster span{color:#333;}#mermaid-svg-lW356GzcnvviyyFt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lW356GzcnvviyyFt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lW356GzcnvviyyFt rect.text{fill:none;stroke-width:0;}#mermaid-svg-lW356GzcnvviyyFt .icon-shape,#mermaid-svg-lW356GzcnvviyyFt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lW356GzcnvviyyFt .icon-shape p,#mermaid-svg-lW356GzcnvviyyFt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lW356GzcnvviyyFt .icon-shape .label rect,#mermaid-svg-lW356GzcnvviyyFt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lW356GzcnvviyyFt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lW356GzcnvviyyFt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lW356GzcnvviyyFt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是





访问 obj.attr
类中 attr 是否是数据描述符?
调用描述符 get
实例 dict 中是否存在 attr?
返回实例属性
类中 attr 是否是非数据描述符?
调用描述符 get
返回普通类属性或继续查找

也可以从类关系角度理解:

text 复制代码
+----------------+
|   User         |
|----------------|
| name = Field   |
| age  = Field   |
+----------------+
        |
        | 属性访问触发
        v
+----------------+
|   Field        |
|----------------|
| __get__        |
| __set__        |
| __set_name__   |
+----------------+

User 负责表达业务模型,Field 负责管理属性规则。这种职责分离,是描述符在工程实践中最迷人的地方。


十一、实战案例:构建一个迷你表单校验系统

假设我们要做一个用户注册表单,需要校验用户名、年龄和邮箱。

先定义基础字段描述符:

python 复制代码
class Field:
    def __init__(self, *, required=True):
        self.required = required
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name

    def validate(self, value):
        if self.required and value is None:
            raise ValueError(f"{self.name} 是必填字段")

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        self.validate(value)
        instance.__dict__[self.name] = value

定义字符串字段:

python 复制代码
class StringField(Field):
    def __init__(self, *, min_length=0, max_length=None, required=True):
        super().__init__(required=required)
        self.min_length = min_length
        self.max_length = max_length

    def validate(self, value):
        super().validate(value)

        if value is None:
            return

        if not isinstance(value, str):
            raise TypeError(f"{self.name} 必须是字符串")

        if len(value) < self.min_length:
            raise ValueError(f"{self.name} 长度不能小于 {self.min_length}")

        if self.max_length is not None and len(value) > self.max_length:
            raise ValueError(f"{self.name} 长度不能大于 {self.max_length}")

定义数字字段:

python 复制代码
class IntegerField(Field):
    def __init__(self, *, min_value=None, max_value=None, required=True):
        super().__init__(required=required)
        self.min_value = min_value
        self.max_value = max_value

    def validate(self, value):
        super().validate(value)

        if value is None:
            return

        if not isinstance(value, int):
            raise TypeError(f"{self.name} 必须是整数")

        if self.min_value is not None and value < self.min_value:
            raise ValueError(f"{self.name} 不能小于 {self.min_value}")

        if self.max_value is not None and value > self.max_value:
            raise ValueError(f"{self.name} 不能大于 {self.max_value}")

使用它们:

python 复制代码
class RegisterForm:
    username = StringField(min_length=3, max_length=20)
    email = StringField(min_length=5)
    age = IntegerField(min_value=0, max_value=150, required=False)

    def __init__(self, username, email, age=None):
        self.username = username
        self.email = email
        self.age = age

测试:

python 复制代码
form = RegisterForm("alice", "alice@example.com", 20)
print(form.username)
print(form.email)
print(form.age)

bad_form = RegisterForm("ab", "x@y.com")
# ValueError: username 长度不能小于 3

这个例子虽然简单,但已经体现了描述符的工程价值:

  • 字段规则集中管理;
  • 业务类代码更清晰;
  • 校验逻辑可以复用;
  • 后续可以扩展错误收集、序列化、默认值、文档生成等能力。

许多成熟框架的模型字段、表单字段、配置字段,本质上都可以从这个思路理解。


十二、描述符常见坑点

1. 不要把数据存在描述符对象自己身上

错误示例:

python 复制代码
class BadDescriptor:
    def __init__(self):
        self.value = None

    def __get__(self, instance, owner):
        return self.value

    def __set__(self, instance, value):
        self.value = value

问题在于:描述符对象属于类,而不是某个实例。多个实例会共享同一个描述符对象。

python 复制代码
class Demo:
    x = BadDescriptor()

a = Demo()
b = Demo()

a.x = 1
b.x = 2

print(a.x)  # 2
print(b.x)  # 2

正确做法是把数据存到 instance.__dict__

python 复制代码
class GoodDescriptor:
    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, owner):
        return instance.__dict__.get(self.name)

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

2. 注意实例为空的情况

__get__ 中要处理 instance is None 的情况:

python 复制代码
def __get__(self, instance, owner):
    if instance is None:
        return self
    return instance.__dict__.get(self.name)

否则通过类访问字段时可能出现异常。

3. 不要为了炫技滥用描述符

描述符很强大,但并不是所有属性管理都需要它。简单计算属性用 property,简单数据结构用普通属性即可。只有当你需要复用属性访问逻辑、做统一校验、延迟加载或框架级封装时,描述符才真正值得使用。


十三、最佳实践建议

第一,给描述符类起清晰的名字。比如 PositiveIntegerStringFieldCachedProperty,不要叫 MagicHandler 这类含义模糊的名字。

第二,优先使用 __set_name__ 管理字段名。过去很多代码会手动传入字段名:

python 复制代码
name = StringField("name")

现在更推荐:

python 复制代码
name = StringField()

然后由 __set_name__ 自动获得属性名,减少重复和出错。

第三,校验逻辑要可组合。比如基础 Field 负责必填校验,StringField 负责字符串校验,IntegerField 负责整数范围校验。这样后续扩展更自然。

第四,错误信息要对人友好。不要只抛出 ValueError("invalid"),而应该明确告诉用户哪个字段、什么规则失败了。

第五,写单元测试。描述符往往隐藏在属性访问背后,一旦出错可能不容易定位。至少应该测试正常赋值、错误类型、边界值、类访问、多个实例互不影响等场景。

示例测试:

python 复制代码
def test_instances_are_independent():
    class User:
        age = IntegerField(min_value=0)

    a = User()
    b = User()

    a.age = 18
    b.age = 30

    assert a.age == 18
    assert b.age == 30

十四、进阶:实现一个 cached_property

有些属性计算成本较高,我们希望第一次访问时计算,之后直接读取缓存。这就是 cached_property 的典型场景。

python 复制代码
class cached_property:
    def __init__(self, func):
        self.func = func
        self.name = func.__name__

    def __get__(self, instance, owner):
        if instance is None:
            return self

        if self.name not in instance.__dict__:
            instance.__dict__[self.name] = self.func(instance)

        return instance.__dict__[self.name]

使用:

python 复制代码
class Report:
    def __init__(self, data):
        self.data = data

    @cached_property
    def total(self):
        print("开始计算 total")
        return sum(self.data)

r = Report([1, 2, 3, 4, 5])

print(r.total)  # 第一次访问,执行计算
print(r.total)  # 第二次访问,直接读取缓存

输出:

python 复制代码
开始计算 total
15
15

这个版本的 cached_property 是非数据描述符。第一次访问时,它把结果写入实例字典。之后由于实例字典优先于非数据描述符,所以再次访问时直接返回缓存值,不再调用 __get__

这正是非数据描述符优先级的巧妙应用。


十五、描述符、装饰器与元类的关系

很多高级 Python 技术并不是孤立存在的。

  • 装饰器常用于包装函数或类;
  • 描述符用于控制属性访问;
  • 元类用于控制类的创建过程。

三者可以组合出非常强大的框架能力。例如,一个 ORM 可能会:

  1. 用描述符定义字段;
  2. 用元类收集字段;
  3. 用装饰器声明路由或事务;
  4. 最终生成 SQL、表单或接口文档。

简化示意:

python 复制代码
class ModelMeta(type):
    def __new__(mcls, name, bases, namespace):
        fields = {}

        for key, value in namespace.items():
            if isinstance(value, Field):
                fields[key] = value

        namespace["_fields"] = fields
        return super().__new__(mcls, name, bases, namespace)

然后:

python 复制代码
class Model(metaclass=ModelMeta):
    pass

class User(Model):
    name = StringField()
    age = IntegerField()

这时 User._fields 就可以自动保存字段信息。虽然这里只是雏形,但你已经看到了框架内部"自动化"的影子。


十六、什么时候应该使用描述符?

适合使用描述符的场景:

  • 多个属性需要相同的校验规则;
  • 需要控制属性读取、写入或删除;
  • 需要实现延迟加载或缓存;
  • 需要构建框架级字段系统;
  • 需要让业务类保持简洁;
  • 需要在属性访问时触发日志、安全检查或转换逻辑。

不太适合使用描述符的场景:

  • 只处理一个简单计算属性;
  • 团队成员对描述符完全不熟悉,维护成本过高;
  • 普通函数或 property 已经足够清晰;
  • 为了"高级"而把简单代码复杂化。

技术的成熟,不在于你知道多少高级语法,而在于你知道什么时候不用它。


十七、总结:描述符是理解 Python 对象模型的钥匙

描述符协议看似冷门,实则贯穿 Python 的许多核心机制。它解释了 property 的工作方式,解释了方法为什么会自动绑定 self,也支撑了大量框架的字段、校验、缓存和延迟加载能力。

对初学者来说,理解描述符可以帮助你看清属性访问背后的逻辑。对资深开发者来说,掌握描述符可以让你写出更优雅、更可复用、更具框架思维的代码。

学习 Python 的过程,有时像是在一层层揭开语言设计的帷幕。刚开始,我们被它简洁的语法吸引;走得更深,才会发现它真正迷人的地方,是那些看似安静却极其强大的底层协议。

描述符就是这样的机制。

它不喧哗,却无处不在。


互动思考

你在项目中是否遇到过需要统一校验属性、缓存计算结果或拦截属性访问的场景?

如果让你设计一个表单校验系统、ORM 字段系统或配置管理工具,你会选择 property、装饰器、描述符,还是元类?

欢迎在评论区分享你的实践经验。也许你的一个问题,正是另一个开发者突破瓶颈的起点。