前置知识: PythonPythonPython

Python 描述符协议:属性访问的底层机制与工程实践

47 minAdvanced2026/7/21

系统阐述 Python 描述符协议的形式化定义、属性查找链、数据与非数据描述符的差异、与元类/property/__slots__ 的协作,以及在 ORM、验证器、缓存属性、观察者模式等场景的工程实践。

Python 描述符协议:属性访问的底层机制与工程实践

描述符协议是 Python 面向对象模型中最核心、却又最常被忽视的机制之一。propertyclassmethodstaticmethodsuper()、ORM 字段、ORM 关系、Pydantic 字段、Django Form 字段,无一不是描述符的应用。本文从形式化定义出发,系统阐述描述符协议的工作原理、属性查找链、数据描述符与非数据描述符的优先级差异、与元类和 __slots__ 的协作,并通过 ORM 字段、类型验证器、缓存属性、观察者模式等工程案例,帮助开发者掌握 Python 元编程的基石。

1. 学习目标

本文依据 Bloom’s Taxonomy(布鲁姆认知目标分类学)的六个层次组织学习目标,确保从低阶认知到高阶创造的渐进式掌握。

1.1 记忆(Remembering)

  • 列出描述符协议的三个核心方法:__get____set____delete__
  • 回忆描述符协议的扩展方法:__set_name__(Python 3.6+)。
  • 列出属性查找链的四个层次:数据描述符、实例属性、非数据描述符、类属性。
  • 陈述数据描述符与非数据描述符的判定条件。

1.2 理解(Understanding)

  • 解释描述符协议为何必须定义为类属性而非实例属性。
  • 描述 obj.attr 访问时 Python 解释器的查找流程。
  • 区分数据描述符(实现 __set____delete__)与非数据描述符(仅实现 __get__)的优先级。
  • 解释 __set_name__ 钩子如何解决描述符”不知道自己叫什么”的问题。

1.3 应用(Applying)

  • 使用描述符实现类型验证器(TypedField)。
  • 使用描述符实现缓存属性(CachedProperty)。
  • 使用描述符实现 ORM 风格的字段定义(CharField、IntegerField)。
  • 使用描述符实现观察者模式(属性变化通知)。

1.4 分析(Analyzing)

  • 分析 property 装饰器与描述符协议的等价关系。
  • 解构 classmethodstaticmethod 的描述符实现。
  • 比较描述符方案与 __slots__、元类方案在属性控制上的差异。
  • 分析描述符在多继承场景下的 MRO 查找行为。

1.5 评价(Evaluating)

  • 评估描述符方案与 property 装饰器在可维护性、可复用性上的优劣。
  • 评判使用描述符存储数据于实例 __dict__ vs 描述符自身的取舍。
  • 评价 Pydantic v2、Django ORM、SQLAlchemy 等框架对描述符的不同使用方式。

1.6 创造(Creating)

  • 设计一套完整的字段验证框架(支持类型、范围、自定义校验)。
  • 实现一个支持惰性加载与缓存失效的描述符。
  • 构建一个基于描述符的领域特定语言(DSL)用于声明式配置。

2. 历史动机与背景

2.1 新式类的诞生(Python 2.2)

在 Python 2.1 及之前,类(class)与类型(type)是两个不同的概念:用户定义的类是 classobj 类型,而内置类型如 intlisttype 类型。这种”二元性”导致经典类(classic class)无法与内置类型统一,也无法正确支持多继承的 MRO(Method Resolution Order)。

2001 年,Guido van Rossum 在 Python 2.2 中引入了新式类(new-style class),通过 PEP 252(Type Unification)和 PEP 253(Subtyping Built-in Types)统一了类与类型:所有用户定义的类都继承自 object,类型即类,类即类型。

新式类引入了一个关键机制:统一属性访问模型。这个模型的核心就是描述符协议。Guido 在《Unifying types and classes in Python 2.2》中阐述:

“The details of attribute access are simplified and unified through the introduction of the descriptor protocol.”

描述符协议最初的目的,是让内置类型(如 intlist)的方法、classmethodstaticmethodpropertysuper() 等机制在用户定义的类中以一致的方式工作。

2.2 描述符协议的设计动机

描述符协议解决的核心问题是:如何在属性访问、赋值、删除时插入自定义逻辑

在描述符协议之前,Python 提供了 __getattr____setattr____delattr__ 来拦截属性访问,但这些方法作用于所有属性,无法针对单个属性定制行为。如果需要对 nameageemail 三个属性分别应用不同的验证逻辑,使用 __setattr__ 会导致大量的 if/elif 分支。

描述符协议将”属性逻辑”封装为独立的对象,每个属性对应一个描述符实例,遵循单一职责原则(SRP)。这种设计使得:

  • 属性逻辑可复用(一个 TypedField(int) 描述符可用于多个类)。
  • 属性逻辑可组合(一个字段可以同时具备类型验证、范围验证、默认值)。
  • 属性逻辑可测试(描述符是独立的类,可单元测试)。

2.3 PEP 487:__set_name__ 钩子(Python 3.6)

在 Python 3.6 之前,描述符面临一个尴尬的问题:描述符不知道自己被绑定的属性名。例如:

class Field:
    def __get__(self, obj, objtype=None):
        # 这里无法知道自己在 User 类中叫 "name" 还是 "age"
        ...

class User:
    name = Field()  # Field 实例不知道自己叫 "name"
    age = Field()

开发者不得不在初始化时手动指定名称:name = Field("name"),这既冗余又容易出错(名称拼写错误)。

PEP 487 引入了 __set_name__(self, owner, name) 钩子,在类创建时由解释器自动调用,告知描述符自己的属性名。这极大简化了描述符的编写:

class Field:
    def __set_name__(self, owner, name):
        self.name = name
        self.private_name = f'_{name}'

__set_name__ 是描述符协议演进的重要里程碑,使得 ORM 字段、Pydantic 字段等框架的 API 更加简洁。

2.4 现代框架中的描述符

描述符协议是现代 Python 元编程的基石,几乎所有重要的框架都依赖它:

  • Django ORMmodels.CharFieldmodels.IntegerFieldmodels.ForeignKey 都是描述符,通过 __get__/__set__ 实现惰性加载与外键关联。
  • SQLAlchemyColumnrelationship 通过描述符实现属性访问的拦截与 SQL 生成。
  • Pydantic v2FieldInfo 通过描述符实现类型验证与序列化。
  • Django Formsforms.CharFieldforms.IntegerField 通过描述符实现表单字段的清洗与验证。
  • attrs / dataclassesattr.ib()field() 内部使用描述符实现默认值与校验。

理解描述符协议,是理解这些框架内部机制、进行高级定制(如自定义字段类型)的前提。

3. 形式化定义

3.1 描述符协议形式化

描述符是一个对象 dd,所属类 DD 满足以下条件之一:

D{__get__}D{__set__,__delete__}D \supseteq \{ \text{`}\_\_get\_\_\text{`} \} \quad \lor \quad D \supseteq \{ \text{`}\_\_set\_\_\text{`}, \text{`}\_\_delete\_\_\text{`} \}

DD 实现了 __get____set____delete__ 中的至少一个方法。描述符对象 dd 的行为由以下三个方法定义:

get:(d,obj,objtype)valueset:(d,obj,value)Nonedelete:(d,obj)None\begin{aligned} \text{get} &: (d, obj, objtype) \to value \\ \text{set} &: (d, obj, value) \to \text{None} \\ \text{delete} &: (d, obj) \to \text{None} \end{aligned}

3.2 数据描述符与非数据描述符

定义数据描述符(Data Descriptor)

isDataDescriptor(d)    hasattr(d,__set__)hasattr(d,__delete__)\text{isDataDescriptor}(d) \iff \text{hasattr}(d, \text{`}\_\_set\_\_\text{`}) \lor \text{hasattr}(d, \text{`}\_\_delete\_\_\text{`})

定义非数据描述符(Non-Data Descriptor)

isNonDataDescriptor(d)    hasattr(d,__get__)¬isDataDescriptor(d)\text{isNonDataDescriptor}(d) \iff \text{hasattr}(d, \text{`}\_\_get\_\_\text{`}) \land \neg \text{isDataDescriptor}(d)

两者的关键差异在于优先级:数据描述符的优先级高于实例属性,非数据描述符的优先级低于实例属性。

3.3 属性查找链形式化

对于表达式 obj.attr,Python 解释器的查找过程可形式化为函数:

lookup(obj,attr)={dataDesc.get(obj,type(obj))if dmro(type(obj)):isDataDescriptor(d)obj.__dict__[attr]if attrobj.__dict__nonDataDesc.get(obj,type(obj))if dmro(type(obj)):isNonDataDescriptor(d)classAttrif attrclassAttrobj.__getattr__(attr)if definedraise AttributeErrorotherwise\text{lookup}(obj, attr) = \begin{cases} \text{dataDesc}.\text{get}(obj, type(obj)) & \text{if } \exists d \in \text{mro}(type(obj)): \text{isDataDescriptor}(d) \\ \text{obj}.\_\_dict\_\_[attr] & \text{if } attr \in \text{obj}.\_\_dict\_\_ \\ \text{nonDataDesc}.\text{get}(obj, type(obj)) & \text{if } \exists d \in \text{mro}(type(obj)): \text{isNonDataDescriptor}(d) \\ \text{classAttr} & \text{if } attr \in \text{classAttr} \\ \text{obj}.\_\_getattr\_\_(attr) & \text{if defined} \\ \text{raise AttributeError} & \text{otherwise} \end{cases}

其中 mro(type(obj))\text{mro}(type(obj))type(obj)type(obj) 的方法解析顺序(Method Resolution Order),按 C3 线性化算法计算。

3.4 赋值与删除形式化

对于赋值 obj.attr = value

assign(obj,attr,value)={dataDesc.set(obj,value)if dmro:isDataDescriptor(d)obj.__setattr__(attr,value)if __setattr__ definedobj.__dict__[attr]=valueotherwise\text{assign}(obj, attr, value) = \begin{cases} \text{dataDesc}.\text{set}(obj, value) & \text{if } \exists d \in \text{mro}: \text{isDataDescriptor}(d) \\ \text{obj}.\_\_setattr\_\_(attr, value) & \text{if } \_\_setattr\_\_ \text{ defined} \\ \text{obj}.\_\_dict\_\_[attr] = value & \text{otherwise} \end{cases}

对于删除 del obj.attr

delete(obj,attr)={dataDesc.delete(obj)if dmro:d has __delete__obj.__delattr__(attr)if __delattr__ defineddel obj.__dict__[attr]otherwise\text{delete}(obj, attr) = \begin{cases} \text{dataDesc}.\text{delete}(obj) & \text{if } \exists d \in \text{mro}: d \text{ has } \_\_delete\_\_ \\ \text{obj}.\_\_delattr\_\_(attr) & \text{if } \_\_delattr\_\_ \text{ defined} \\ \text{del obj}.\_\_dict\_\_[attr] & \text{otherwise} \end{cases}

3.5 __set_name__ 钩子形式化

类创建时,解释器对类字典中的每个描述符调用钩子:

attrclassDict:if hasattr(classDict[attr],__set_name__)    classDict[attr].__set_name__(cls,attr)\forall attr \in \text{classDict}: \text{if hasattr}(classDict[attr], \text{`}\_\_set\_name\_\_\text{`}) \implies classDict[attr].\_\_set\_name\_\_(cls, attr)

这个钩子在 type.__new__ 中触发,发生在 __init_subclass__ 之前。

4. 理论推导

4.1 属性查找链的优先级证明

命题:数据描述符的优先级高于实例属性,实例属性的优先级高于非数据描述符。

证明:通过反证法。

假设非数据描述符 DndD_{nd} 的优先级高于实例属性。那么对于以下代码:

class D:
    def __get__(self, obj, objtype=None):
        return "descriptor"

class C:
    d = D()
    def __init__(self):
        self.d = "instance"

c = C()
print(c.d)  # 应该输出什么?

如果非数据描述符优先级高于实例属性,则 c.d 应输出 "descriptor"。但实际输出为 "instance",因为 __init__self.d = "instance" 覆盖了实例属性。

这一设计使得 classmethodstaticmethodproperty 等非数据描述符可以被实例属性”遮蔽”,提供了灵活性。而数据描述符(如 property 同时定义 __set__)则不可被遮蔽,确保了属性的访问逻辑不被绕过。

4.2 C3 线性化与描述符查找

描述符查找发生在 MRO 上,C3 线性化保证了多继承下查找顺序的一致性。对于类 CC 的 MRO:

mro(C)=[C]+merge(mro(B1),mro(B2),...,[B1,B2,...])\text{mro}(C) = [C] + \text{merge}(\text{mro}(B_1), \text{mro}(B_2), ..., [B_1, B_2, ...])

其中 merge\text{merge} 算法保证:若类 XXmro(C)\text{mro}(C) 中出现在 YY 之前,则在所有涉及 XXYY 的父类 MRO 中 XX 都在 YY 之前。

描述符查找时,解释器按 mro(C)\text{mro}(C) 顺序查找,第一个匹配的描述符胜出。这解释了为什么子类可以覆盖父类的描述符:

class Base:
    x = SomeDescriptor()

class Derived(Base):
    x = AnotherDescriptor()  # 覆盖父类的描述符

4.3 数据存储位置的复杂性分析

描述符有两种数据存储策略:

  1. 存储于实例 __dict__:每个实例独立持有数据,描述符仅作为访问代理。

    • 时间复杂度:O(1)O(1)(字典查找)。
    • 空间复杂度:O(n)O(n)nn 个实例)。
  2. 存储于描述符自身:所有实例共享同一份数据,通过 id(obj)WeakKeyDictionary 区分。

    • 时间复杂度:O(1)O(1)(哈希查找)或 O(logn)O(\log n)(树查找)。
    • 空间复杂度:O(n)O(n)(描述符持有 nn 个键值对)。

策略 1 是常见做法,但要求实例有 __dict__(即未使用 __slots__)。策略 2 适用于 __slots__ 场景或需要跨实例共享元数据的场景。

4.4 __slots__ 与描述符的协作

__slots__ 是 Python 的内存优化机制,它禁用实例的 __dict__,改为固定槽位存储。__slots__ 本质上是数据描述符

class C:
    __slots__ = ('x', 'y')

等价于解释器自动生成:

class C:
    # member_descriptor 是 C 中预定义的描述符类
    x = member_descriptor(...)
    y = member_descriptor(...)

由于 member_descriptor 是数据描述符(实现了 __set____delete__),其优先级高于实例属性。但实例没有 __dict__,因此描述符无法将数据存入 __dict__,而是存入预分配的槽位。

这一机制意味着:自定义描述符在 __slots__ 类中无法将数据存入实例 __dict__,必须改用描述符自身的字典或 WeakKeyDictionary

4.5 property 的描述符本质

property 是 Python 内置的描述符工厂,其等价实现为:

class Property:
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        self.__doc__ = doc

    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):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

由于 property 同时定义了 __get____set____delete__,它是数据描述符,优先级高于实例属性。这就是为什么 @property 装饰的属性无法在实例上被覆盖的原因。

4.6 classmethodstaticmethod 的描述符实现

classmethodstaticmethod 都是非数据描述符(仅实现 __get__),因此可以被实例属性遮蔽。

class StaticMethod:
    def __init__(self, func):
        self.func = func

    def __get__(self, obj, objtype=None):
        return self.func

class ClassMethod:
    def __init__(self, func):
        self.func = func

    def __get__(self, obj, objtype=None):
        if objtype is None:
            objtype = type(obj)
        return lambda *args: self.func(objtype, *args)

classmethod__get__ 返回一个绑定了类的方法,staticmethod__get__ 返回原函数本身。这种设计使得方法可以被实例属性覆盖(虽然不推荐)。

5. 代码示例

5.1 类型验证描述符(TypedField)

以下示例实现一个可复用的类型验证描述符,用于确保属性值符合指定类型:

from typing import Type, Any, Optional

class TypedField:
    """
    类型验证描述符:确保属性值符合指定类型
    
    输入参数:
        expected_type (Type): 期望的属性类型
        default (Any): 默认值,未设置时返回
        nullable (bool): 是否允许为 None
    
    核心执行流程:
        1. __set_name__ 钩子自动获取属性名
        2. __get__ 从实例 __dict__ 读取值,未设置时返回默认值
        3. __set__ 验证类型后存入实例 __dict__
    """

    def __init__(self, expected_type: Type, default: Any = None, nullable: bool = True):
        # 期望类型,用于 isinstance 检查
        self.expected_type = expected_type
        # 默认值,未设置时返回
        self.default = default
        # 是否允许 None
        self.nullable = nullable
        # 私有属性名,存入实例 __dict__ 时使用
        self._private_name: Optional[str] = None

    def __set_name__(self, owner: type, name: str) -> None:
        """
        Python 3.6+ 钩子,类创建时自动调用
        参数:
            owner: 拥有该描述符的类
            name: 描述符在类中的属性名
        """
        self._private_name = f'_typed_{name}'
        self.name = name

    def __get__(self, obj: Any, objtype: Optional[type] = None) -> Any:
        """
        读取属性时调用
        参数:
            obj: 访问属性的实例,类访问时为 None
            objtype: 实例的类
        返回:
            属性值或默认值
        """
        # 类访问时返回描述符自身
        if obj is None:
            return self
        # 从实例 __dict__ 读取,未设置时返回默认值
        return getattr(obj, self._private_name, self.default)

    def __set__(self, obj: Any, value: Any) -> None:
        """
        赋值属性时调用,包含类型验证
        参数:
            obj: 访问属性的实例
            value: 要赋的值
        异常:
            TypeError: 类型不匹配且非 None
        """
        # None 处理
        if value is None:
            if not self.nullable:
                raise TypeError(f"{self.name} 不允许为 None")
            setattr(obj, self._private_name, None)
            return
        # 类型检查
        if not isinstance(value, self.expected_type):
            raise TypeError(
                f"{self.name} 期望类型 {self.expected_type.__name__}, "
                f"实际类型 {type(value).__name__}"
            )
        # 通过验证,存入实例 __dict__
        setattr(obj, self._private_name, value)


class User:
    """使用 TypedField 定义带类型校验的用户类"""
    # 字符串类型的 name 字段,不允许为 None
    name = TypedField(str, default="", nullable=False)
    # 整数类型的 age 字段
    age = TypedField(int, default=0)
    # 浮点类型的 score 字段
    score = TypedField(float, default=0.0)


# 使用示例
user = User()
user.name = "张三"  # 正确
user.age = 25       # 正确
user.score = 95.5   # 正确

try:
    user.age = "25"  # TypeError: age 期望类型 int, 实际类型 str
except TypeError as e:
    print(f"类型错误: {e}")

# 类访问返回描述符自身
print(type(User.name))  # <class '...TypedField'>

5.2 范围验证描述符(RangeField)

from typing import Optional, Union

class RangeField:
    """
    范围验证描述符:确保数值属性在指定范围内
    
    输入参数:
        min_value: 最小值(可选)
        max_value: 最大值(可选)
        default: 默认值
    
    核心执行流程:
        1. __set_name__ 钩子获取属性名
        2. __set__ 验证值是否在 [min_value, max_value] 区间
        3. 验证通过后存入实例 __dict__
    """

    def __init__(
        self,
        min_value: Optional[Union[int, float]] = None,
        max_value: Optional[Union[int, float]] = None,
        default: Union[int, float] = 0
    ):
        self.min_value = min_value
        self.max_value = max_value
        self.default = default
        self._private_name: Optional[str] = None

    def __set_name__(self, owner: type, name: str) -> None:
        self._private_name = f'_range_{name}'
        self.name = name

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self._private_name, self.default)

    def __set__(self, obj, value):
        # 类型检查
        if not isinstance(value, (int, float)):
            raise TypeError(f"{self.name} 必须是数值类型, 实际 {type(value).__name__}")
        # 范围检查
        if self.min_value is not None and value < self.min_value:
            raise ValueError(f"{self.name} 不能小于 {self.min_value}, 实际 {value}")
        if self.max_value is not None and value > self.max_value:
            raise ValueError(f"{self.name} 不能大于 {self.max_value}, 实际 {value}")
        # 存储值
        setattr(obj, self._private_name, value)


class Student:
    """学生类,使用 RangeField 限制属性范围"""
    age = RangeField(min_value=0, max_value=150, default=18)
    score = RangeField(min_value=0, max_value=100, default=60)
    height = RangeField(min_value=0, max_value=300, default=170)


student = Student()
student.age = 20       # 正确
student.score = 95     # 正确
student.height = 175   # 正确

try:
    student.age = -1   # ValueError: age 不能小于 0
except ValueError as e:
    print(f"范围错误: {e}")

try:
    student.score = 150  # ValueError: score 不能大于 100
except ValueError as e:
    print(f"范围错误: {e}")

5.3 缓存属性描述符(CachedProperty)

from typing import Callable, Any, Optional

class CachedProperty:
    """
    缓存属性描述符:首次访问时计算结果并缓存到实例 __dict__
    
    由于该描述符仅实现 __get__,是非数据描述符,优先级低于实例属性,
    因此一旦值被存入实例 __dict__,后续访问将直接读取实例属性,
    跳过描述符的 __get__ 调用,实现 O(1) 的缓存命中。
    
    输入参数:
        func (Callable): 计算属性的函数
    
    核心执行流程:
        1. 首次访问:调用 func 计算结果,存入实例 __dict__
        2. 后续访问:直接从实例 __dict__ 读取(不经过 __get__)
    
    注意:
        由于非数据描述符的特性,缓存的值无法被 __set__ 拦截,
        如需缓存失效,需配合 __delete__ 实现数据描述符版本。
    """

    def __init__(self, func: Callable):
        self.func = func
        self.name = func.__name__
        self.__doc__ = func.__doc__

    def __get__(self, obj: Any, objtype: Optional[type] = None) -> Any:
        if obj is None:
            return self
        # 计算结果
        value = self.func(obj)
        # 存入实例 __dict__,后续访问将直接读取实例属性
        obj.__dict__[self.name] = value
        return value


class DataProcessor:
    """数据处理类,演示 CachedProperty 的使用"""

    def __init__(self, data: list):
        self.data = data

    @CachedProperty
    def sum_squared(self) -> float:
        """计算平方和(耗时操作,仅首次访问时执行)"""
        print("执行耗时计算 sum_squared...")
        return sum(x ** 2 for x in self.data)

    @CachedProperty
    def mean(self) -> float:
        """计算均值"""
        print("执行耗时计算 mean...")
        return sum(self.data) / len(self.data) if self.data else 0

    @CachedProperty
    def variance(self) -> float:
        """计算方差"""
        print("执行耗时计算 variance...")
        m = self.mean  # 复用缓存
        return sum((x - m) ** 2 for x in self.data) / len(self.data) if self.data else 0


# 使用示例
processor = DataProcessor(list(range(1000)))

print("第一次访问 sum_squared:")
print(processor.sum_squared)  # 执行计算
print("第二次访问 sum_squared:")
print(processor.sum_squared)  # 直接读取缓存,无打印

# 检查实例 __dict__ 中的缓存
print(f"实例缓存: {list(processor.__dict__.keys())}")

5.4 可失效的缓存属性(数据描述符版本)

from typing import Callable, Any, Optional

class ClearableCachedProperty:
    """
    可失效的缓存属性:数据描述符版本,支持 del 清除缓存
    
    与 CachedProperty 不同,本描述符同时实现 __get__ 和 __delete__,
    属于数据描述符,优先级高于实例属性,因此即使缓存存入实例 __dict__,
    访问时仍会经过 __get__,可通过 __delete__ 主动失效。
    """

    def __init__(self, func: Callable):
        self.func = func
        self.name = func.__name__
        self.__doc__ = func.__doc__

    def __get__(self, obj: Any, objtype: Optional[type] = None) -> Any:
        if obj is None:
            return self
        # 尝试从实例 __dict__ 读取缓存
        if self.name in obj.__dict__:
            return obj.__dict__[self.name]
        # 缓存未命中,计算并存入
        value = self.func(obj)
        obj.__dict__[self.name] = value
        return value

    def __delete__(self, obj: Any) -> None:
        """删除缓存,下次访问时重新计算"""
        if self.name in obj.__dict__:
            del obj.__dict__[self.name]


class ImageProcessor:
    """图像处理类,演示可失效缓存"""

    def __init__(self, image_path: str):
        self.image_path = image_path
        self._modified = False

    @ClearableCachedProperty
    def thumbnail(self):
        """生成缩略图(耗时操作)"""
        print(f"生成缩略图: {self.image_path}")
        # 模拟耗时操作
        import time
        time.sleep(0.1)
        return f"thumbnail_{self.image_path}"

    def apply_filter(self, filter_name: str):
        """应用滤镜,修改原图后需要失效缩略图缓存"""
        print(f"应用滤镜: {filter_name}")
        self._modified = True
        # 失效缩略图缓存
        del self.thumbnail


processor = ImageProcessor("photo.jpg")
print("首次访问缩略图:")
print(processor.thumbnail)  # 执行计算
print("再次访问:")
print(processor.thumbnail)  # 读取缓存
print("应用滤镜后:")
processor.apply_filter("sepia")  # 失效缓存
print("失效后访问:")
print(processor.thumbnail)  # 重新计算

5.5 ORM 风格的字段定义

from typing import Any, Optional, Type, Callable

class Field:
    """
    ORM 字段描述符基类:模拟 Django ORM 的字段定义
    
    核心执行流程:
        1. __init__ 接收字段配置(是否可空、默认值、校验器)
        2. __set_name__ 自动获取字段名
        3. __get__/__set__ 拦截属性访问,执行校验
    """

    def __init__(
        self,
        primary_key: bool = False,
        nullable: bool = True,
        default: Any = None,
        validators: Optional[list] = None
    ):
        # 是否为主键
        self.primary_key = primary_key
        # 是否允许为空
        self.nullable = nullable
        # 默认值
        self.default = default
        # 自定义校验器列表
        self.validators = validators or []
        # 字段名与私有名
        self.name: Optional[str] = None
        self._private_name: Optional[str] = None

    def __set_name__(self, owner: type, name: str) -> None:
        self.name = name
        self._private_name = f'_field_{name}'

    def __get__(self, obj: Any, objtype: Optional[type] = None) -> Any:
        if obj is None:
            return self
        return getattr(obj, self._private_name, self.default)

    def __set__(self, obj: Any, value: Any) -> None:
        # 执行字段级校验
        self.validate(value)
        # 执行自定义校验器
        for validator in self.validators:
            validator(value)
        # 存储值
        setattr(obj, self._private_name, value)

    def validate(self, value: Any) -> None:
        """字段级校验,子类可覆盖"""
        if value is None:
            if not self.nullable:
                raise ValueError(f"{self.name} 不能为空")
            return


class CharField(Field):
    """字符串字段"""

    def __init__(self, max_length: int = 255, **kwargs):
        super().__init__(**kwargs)
        self.max_length = max_length

    def validate(self, value: Any) -> None:
        super().validate(value)
        if value is None:
            return
        if not isinstance(value, str):
            raise TypeError(f"{self.name} 必须是 str, 实际 {type(value).__name__}")
        if len(value) > self.max_length:
            raise ValueError(f"{self.name} 长度 {len(value)} 超过最大 {self.max_length}")


class IntegerField(Field):
    """整数字段"""

    def __init__(self, min_value: Optional[int] = None, max_value: Optional[int] = None, **kwargs):
        super().__init__(**kwargs)
        self.min_value = min_value
        self.max_value = max_value

    def validate(self, value: Any) -> None:
        super().validate(value)
        if value is None:
            return
        if not isinstance(value, int) or isinstance(value, bool):
            raise TypeError(f"{self.name} 必须是 int, 实际 {type(value).__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}")


class BooleanField(Field):
    """布尔字段"""

    def validate(self, value: Any) -> None:
        super().validate(value)
        if value is None:
            return
        if not isinstance(value, bool):
            raise TypeError(f"{self.name} 必须是 bool, 实际 {type(value).__name__}")


class ModelMeta(type):
    """
    模型元类:自动收集字段,便于序列化与数据库映射
    """

    def __new__(mcs, name: str, bases: tuple, namespace: dict):
        # 收集本类定义的字段(不包含父类的字段)
        fields = {}
        for key, value in namespace.items():
            if isinstance(value, Field):
                fields[key] = value
        # 合并父类的字段
        for base in bases:
            if hasattr(base, '_fields'):
                fields.update(base._fields)
        namespace['_fields'] = fields
        return super().__new__(mcs, name, bases, namespace)


class Model(metaclass=ModelMeta):
    """模型基类"""
    _fields: dict = {}

    def __init__(self, **kwargs):
        # 设置字段默认值
        for field_name, field in self._fields.items():
            setattr(self, field_name, field.default)
        # 应用传入的值
        for key, value in kwargs.items():
            if key in self._fields:
                setattr(self, key, value)
            else:
                raise TypeError(f"未知字段: {key}")

    def to_dict(self) -> dict:
        """序列化为字典"""
        return {name: getattr(self, name) for name in self._fields}

    def __repr__(self) -> str:
        attrs = ', '.join(f"{k}={v!r}" for k, v in self.to_dict().items())
        return f"{type(self).__name__}({attrs})"


class User(Model):
    """用户模型"""
    id = IntegerField(primary_key=True, nullable=False)
    name = CharField(max_length=100, nullable=False)
    age = IntegerField(min_value=0, max_value=150, default=18)
    is_active = BooleanField(default=True)


# 使用示例
user = User(id=1, name="张三", age=25, is_active=True)
print(user)  # User(id=1, name='张三', age=25, is_active=True)

try:
    user.age = -1  # ValueError: age 不能小于 0
except ValueError as e:
    print(f"校验失败: {e}")

try:
    user.name = "A" * 200  # ValueError: name 长度 200 超过最大 100
except ValueError as e:
    print(f"校验失败: {e}")

# 序列化
print(user.to_dict())  # {'id': 1, 'name': '张三', 'age': 25, 'is_active': True}

5.6 观察者模式描述符

from typing import Callable, Any, Optional, List

class ObservableField:
    """
    可观察属性描述符:值变化时通知所有观察者
    
    输入参数:
        default: 默认值
    
    核心执行流程:
        1. __set__ 检测值变化
        2. 若值变化,调用所有注册的观察者回调
    """

    def __init__(self, default: Any = None):
        self.default = default
        self._private_name: Optional[str] = None

    def __set_name__(self, owner: type, name: str) -> None:
        self._private_name = f'_observable_{name}'
        self.name = name

    def __get__(self, obj: Any, objtype: Optional[type] = None) -> Any:
        if obj is None:
            return self
        return getattr(obj, self._private_name, self.default)

    def __set__(self, obj: Any, value: Any) -> None:
        # 获取旧值
        old_value = getattr(obj, self._private_name, self.default)
        # 存储新值
        setattr(obj, self._private_name, value)
        # 仅在值变化时通知
        if old_value != value:
            self._notify_observers(obj, old_value, value)

    def _notify_observers(self, obj: Any, old_value: Any, new_value: Any) -> None:
        """通知所有观察者"""
        observers: List[Callable] = getattr(obj, '_observers', [])
        for callback in observers:
            callback(self.name, old_value, new_value)


class Observable:
    """可观察对象基类"""

    def __init__(self):
        # 观察者列表
        self._observers: List[Callable] = []

    def add_observer(self, callback: Callable) -> None:
        """注册观察者"""
        self._observers.append(callback)

    def remove_observer(self, callback: Callable) -> None:
        """移除观察者"""
        if callback in self._observers:
            self._observers.remove(callback)


class Order(Observable):
    """订单类,状态变化时通知观察者"""
    status = ObservableField(default="pending")
    amount = ObservableField(default=0.0)


# 使用示例
def log_status_change(attr: str, old: Any, new: Any) -> None:
    print(f"[日志] {attr} 变化: {old} -> {new}")

def send_notification(attr: str, old: Any, new: Any) -> None:
    if attr == 'status' and new == 'shipped':
        print("[通知] 订单已发货,邮件通知客户")

order = Order()
order.add_observer(log_status_change)
order.add_observer(send_notification)

print("--- 修改状态 ---")
order.status = "paid"       # 触发日志
order.status = "shipped"    # 触发日志与通知

print("--- 修改金额 ---")
order.amount = 99.9         # 触发日志

5.7 只读属性描述符

from typing import Any, Optional

class ReadOnly:
    """
    只读属性描述符:初始化后不可修改
    
    核心执行流程:
        1. 首次 __set__ 允许赋值
        2. 后续 __set__ 抛出 AttributeError
    """

    def __init__(self, name: Optional[str] = None):
        self.name = name
        self._private_name: Optional[str] = None

    def __set_name__(self, owner: type, name: str) -> None:
        self.name = name
        self._private_name = f'_readonly_{name}'

    def __get__(self, obj: Any, objtype: Optional[type] = None) -> Any:
        if obj is None:
            return self
        return getattr(obj, self._private_name, None)

    def __set__(self, obj: Any, value: Any) -> None:
        # 若已设置,则禁止修改
        if hasattr(obj, self._private_name):
            raise AttributeError(f"'{self.name}' 是只读属性,不可修改")
        setattr(obj, self._private_name, value)


class Configuration:
    """配置类,关键属性只读"""
    api_key = ReadOnly()
    base_url = ReadOnly()
    timeout = ReadOnly()

    def __init__(self, api_key: str, base_url: str, timeout: int = 30):
        # 初始化时赋值,允许设置
        self.api_key = api_key
        self.base_url = base_url
        self.timeout = timeout


# 使用示例
config = Configuration(
    api_key="sk-12345",
    base_url="https://api.example.com",
    timeout=60
)

print(f"API Key: {config.api_key}")
print(f"Base URL: {config.base_url}")

try:
    config.api_key = "sk-67890"  # AttributeError
except AttributeError as e:
    print(f"错误: {e}")

5.8 惰性加载描述符(带 WeakKeyDictionary)

from typing import Callable, Any, Optional
import weakref

class LazyAttribute:
    """
    惰性加载描述符:使用 WeakKeyDictionary 存储每个实例的值
    
    适用场景:
        - 实例使用 __slots__,无法存入 __dict__
        - 需要跨实例共享描述符,但每个实例独立持有值
        - 希望实例销毁时自动清理描述符中的引用
    
    核心执行流程:
        1. __get__ 检查 WeakKeyDictionary 是否有该实例的值
        2. 若无,调用 loader 加载,存入 WeakKeyDictionary
        3. 实例被垃圾回收时,WeakKeyDictionary 自动清理
    
    注意:
        WeakKeyDictionary 要求键(实例)支持弱引用,
        使用 __slots__ 时需在 __slots__ 中包含 '__weakref__'。
    """

    def __init__(self, loader: Callable):
        self.loader = loader
        self.name = loader.__name__
        # 使用弱引用字典,实例销毁时自动清理
        self._storage = weakref.WeakKeyDictionary()

    def __get__(self, obj: Any, objtype: Optional[type] = None) -> Any:
        if obj is None:
            return self
        # 尝试从存储中读取
        if obj in self._storage:
            return self._storage[obj]
        # 加载并存储
        value = self.loader(obj)
        self._storage[obj] = value
        return value

    def __delete__(self, obj: Any) -> None:
        """清除缓存"""
        if obj in self._storage:
            del self._storage[obj]


class SlottedClass:
    """
    使用 __slots__ 的类,演示 LazyAttribute 的弱引用存储
    
    __slots__ 中包含 '__weakref__' 以支持弱引用
    """
    __slots__ = ('id', '__weakref__')

    def __init__(self, id: int):
        self.id = id

    @LazyAttribute
    def expensive_data(self):
        """模拟耗时计算"""
        print(f"计算实例 {self.id} 的数据...")
        return [i ** 2 for i in range(self.id)]


# 使用示例
obj1 = SlottedClass(5)
obj2 = SlottedClass(10)

print("首次访问 obj1:")
print(obj1.expensive_data)  # 计算并缓存
print("再次访问 obj1:")
print(obj1.expensive_data)  # 读取缓存

print("首次访问 obj2:")
print(obj2.expensive_data)  # 计算并缓存

# 删除 obj1,WeakKeyDictionary 自动清理
del obj1
# 此处 storage 中 obj1 的条目已被自动移除

6. 对比分析

6.1 描述符 vs property 装饰器

维度描述符property 装饰器
本质实现 __get__/__set__/__delete__ 的类描述符的语法糖(内部仍为描述符)
可复用性高(一个描述符类可用于多个属性)低(每个属性需单独定义)
可组合性高(可组合多个描述符)低(每个 property 独立)
代码量描述符类定义一次,多处使用每个属性需 4-6 行代码
可读性中等(需理解描述符协议)高(语法直观)
适用场景多属性共享逻辑(ORM、验证器)单属性定制(getter/setter)
学习曲线陡峭平缓

讨论property 是描述符的特例,适用于属性数量少、逻辑简单的场景。当多个属性共享相同逻辑(如类型验证、范围验证)时,描述符更具优势。Django ORM、Pydantic 等框架选择描述符而非 property,正是因为框架需要为大量字段提供统一行为。

6.2 数据描述符 vs 非数据描述符

维度数据描述符非数据描述符
定义实现 __set____delete__仅实现 __get__
优先级高于实例属性低于实例属性
可遮蔽性不可被实例属性遮蔽可被实例属性遮蔽
典型应用property、ORM 字段、__slots__classmethodstaticmethodCachedProperty
缓存友好性低(每次访问经过 __get__高(存入 __dict__ 后跳过 __get__
可控性高(赋值、删除均可拦截)低(赋值直接写入 __dict__

讨论:非数据描述符的”可遮蔽性”是 CachedProperty 工作的基础——首次计算后将值存入实例 __dict__,后续访问直接读取实例属性,跳过描述符的 __get__,实现 O(1) 缓存命中。数据描述符则适用于需要严格控制属性访问的场景(如 ORM 字段需在赋值时触发校验)。

6.3 描述符 vs 元类

维度描述符元类
作用层实例属性访问类创建
触发时机obj.attr 访问时class 定义时
典型用途属性验证、缓存、ORM 字段字段收集、类注册、API 生成
复杂度中等
与框架关系Django ORM 字段、Pydantic 字段Django ModelBase、SQLAlchemy declarative_base
组合使用常与元类协作(元类收集描述符)独立使用

讨论:元类与描述符经常协作。Django ORM 中,ModelBase 元类扫描类的 Field 描述符,构建 _meta.fields 元数据;描述符则负责运行时的属性访问拦截。两者分工明确:元类管理”类层面”的结构,描述符管理”实例层面”的行为。

6.4 描述符 vs __slots__

维度描述符__slots__
目的自定义属性访问逻辑节省内存(禁用 __dict__
实现实现 __get__/__set__解释器自动生成数据描述符
存储位置通常实例 __dict__预分配槽位
兼容性__dict__ 共存禁用 __dict__(默认)
内存节省显著(每实例节省 100+ 字节)
属性限制不限制属性数量限制为 __slots__ 列出的属性

讨论__slots__ 与自定义描述符存在兼容性问题——自定义描述符默认将数据存入实例 __dict__,而 __slots__ 类没有 __dict__。解决方法:(1)描述符使用 WeakKeyDictionary 存储数据;(2)在 __slots__ 中预留私有属性名(如 _field_name)。

6.5 property 的描述符本质验证

以下代码验证 property 是数据描述符:

class C:
    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

# 检查 property 的属性
print(hasattr(C.__dict__['x'], '__get__'))   # True
print(hasattr(C.__dict__['x'], '__set__'))   # True
print(hasattr(C.__dict__['x'], '__delete__'))  # True(property 默认实现)

# 验证数据描述符优先级
c = C()
c.x = 1  # 通过 setter 设置
print(c.x)  # 1

# 尝试用实例属性遮蔽(应失败,因为 property 是数据描述符)
try:
    c.__dict__['x'] = 999  # 直接写入实例 __dict__
    print(c.x)  # 仍输出 1,因为 property 优先级更高
except Exception as e:
    print(e)

7. 常见陷阱与反模式

7.1 描述符定义为实例属性(不生效)

反模式

class BadDescriptor:
    def __get__(self, obj, objtype=None):
        return "descriptor"

class C:
    def __init__(self):
        # 错误:描述符定义为实例属性,协议不生效
        self.attr = BadDescriptor()

c = C()
print(c.attr)  # 输出 <BadDescriptor object>,而非 "descriptor"

原因:描述符协议仅对类属性生效。解释器在 type(obj).__mro__ 中查找描述符,而非 obj.__dict__。实例属性中的描述符对象被视为普通对象,不触发 __get__

正确做法

class C:
    attr = BadDescriptor()  # 类属性

c = C()
print(c.attr)  # 输出 "descriptor"

7.2 描述符自身存储数据导致实例间共享

反模式

class SharedField:
    def __init__(self):
        self.value = None  # 错误:数据存在描述符自身

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return self.value

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

class User:
    name = SharedField()

u1 = User()
u2 = User()
u1.name = "张三"
print(u2.name)  # 输出 "张三",而非 None,因为所有实例共享描述符

原因:描述符是类属性,所有实例共享同一个描述符对象。若数据存在描述符自身的 __dict__ 中,所有实例访问的是同一份数据。

正确做法:将数据存入实例 __dict__

class CorrectField:
    def __set_name__(self, owner, name):
        self._private_name = f'_{name}'

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self._private_name, None)

    def __set__(self, obj, value):
        setattr(obj, self._private_name, value)

7.3 __get__ 中未处理 obj is None 的情况

反模式

class BadField:
    def __get__(self, obj, objtype=None):
        # 错误:未处理类访问(obj is None)
        return obj._value  # 类访问时 obj 为 None,抛出 AttributeError

class C:
    field = BadField()

print(C.field)  # AttributeError: 'NoneType' object has no attribute '_value'

正确做法

class GoodField:
    def __get__(self, obj, objtype=None):
        if obj is None:
            # 类访问时返回描述符自身
            return self
        return obj._value

7.4 在 __slots__ 类中使用存入 __dict__ 的描述符

反模式

class Field:
    def __set_name__(self, owner, name):
        self._private_name = f'_{name}'

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self._private_name, None)

    def __set__(self, obj, value):
        setattr(obj, self._private_name, value)

class SlottedUser:
    __slots__ = ()  # 未预留 _name 槽位

    name = Field()

u = SlottedUser()
try:
    u.name = "张三"  # AttributeError: 'SlottedUser' object has no attribute '_name'
except AttributeError as e:
    print(e)

原因__slots__ 类无 __dict__,描述符尝试用 setattr 设置 _name 属性时,因槽位不存在而失败。

正确做法:(1)在 __slots__ 中预留私有属性名;(2)使用 WeakKeyDictionary 存储。

class SlottedUser:
    __slots__ = ('_name',)  # 预留槽位

    name = Field()

u = SlottedUser()
u.name = "张三"
print(u.name)  # 张三

7.5 使用描述符实现单例模式导致内存泄漏

事故案例:某服务使用描述符缓存数据库连接池,未使用弱引用,导致连接池永不释放。

class ConnectionPool:
    """反模式:使用普通 dict 存储连接池"""
    _pools = {}  # 全局字典,永不释放

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if obj not in self._pools:
            self._pools[obj] = create_pool(obj.config)
        return self._pools[obj]

后果:服务运行一周后,_pools 字典累积 10000+ 条目,每个连接池持有 5-10 个数据库连接,导致内存占用从 200MB 增长至 2GB,触发 OOM。

修复方案:使用 WeakKeyDictionary

import weakref

class ConnectionPool:
    _pools = weakref.WeakKeyDictionary()  # 实例销毁时自动清理

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if obj not in self._pools:
            self._pools[obj] = create_pool(obj.config)
        return self._pools[obj]

7.6 描述符与 __init__ 中的赋值顺序问题

事故案例:某 ORM 在 __init__ 中设置字段值时,因依赖其他字段未初始化而失败。

class Field:
    def __set_name__(self, owner, name):
        self._private_name = f'_{name}'

    def __set__(self, obj, value):
        # 错误:__set__ 中依赖了 obj 的其他属性
        if obj._initialized:  # 但 _initialized 尚未设置
            validate(value)
        setattr(obj, self._private_name, value)

class User:
    name = Field()

    def __init__(self, name):
        self.name = name  # AttributeError: '_initialized' not set
        self._initialized = True

修复方案:(1)使用 hasattr 检查依赖;(2)调整初始化顺序;(3)将依赖标记为类属性默认值。

7.7 多继承下的描述符覆盖问题

反模式

class FieldA:
    def __get__(self, obj, objtype=None):
        return "A"

class FieldB:
    def __get__(self, obj, objtype=None):
        return "B"

class Base1:
    x = FieldA()

class Base2:
    x = FieldB()

class Derived(Base1, Base2):
    pass

d = Derived()
print(d.x)  # 输出 "A",因为 Base1 在 MRO 中先于 Base2

讨论:多继承下,MRO 顺序决定描述符查找顺序。若开发者期望 FieldB 优先,需调整继承顺序或显式指定。这种隐式覆盖容易导致难以排查的 bug,建议在文档中明确描述符的优先级。

7.8 性能陷阱:过度使用数据描述符

事故案例:某高频交易系统将所有属性定义为数据描述符,每次属性访问都经过 __get__,导致吞吐量下降 30%。

分析:数据描述符的 __get__ 在每次访问时调用,而非数据描述符(如 CachedProperty)在首次计算后存入实例 __dict__,后续访问直接读取实例属性,跳过描述符调用。对于高频访问的属性,应优先使用非数据描述符或直接访问。

优化:对只读且计算成本高的属性使用 CachedProperty(非数据描述符);对需要校验的属性使用数据描述符,但避免在 __get__ 中执行复杂逻辑。

8. 工程实践

8.1 生产级字段验证框架

以下是一个生产级的字段验证框架,支持类型、范围、自定义校验器,并提供详细的错误信息:

from typing import Any, Optional, Type, Callable, List, Union
from dataclasses import dataclass

@dataclass
class ValidationError:
    """校验错误信息"""
    field: str
    message: str
    code: str

    def __str__(self):
        return f"[{self.field}] {self.message} (code: {self.code})"


class Validator:
    """校验器基类"""

    def validate(self, value: Any) -> Optional[str]:
        """
        校验值,返回错误信息(None 表示通过)
        """
        raise NotImplementedError

    def __call__(self, value: Any) -> Optional[str]:
        return self.validate(value)


class TypeValidator(Validator):
    """类型校验器"""

    def __init__(self, expected_type: Type):
        self.expected_type = expected_type

    def validate(self, value: Any) -> Optional[str]:
        if not isinstance(value, self.expected_type):
            return (
                f"期望类型 {self.expected_type.__name__}, "
                f"实际 {type(value).__name__}"
            )
        return None


class RangeValidator(Validator):
    """范围校验器"""

    def __init__(
        self,
        min_value: Optional[Union[int, float]] = None,
        max_value: Optional[Union[int, float]] = None
    ):
        self.min_value = min_value
        self.max_value = max_value

    def validate(self, value: Any) -> Optional[str]:
        if self.min_value is not None and value < self.min_value:
            return f"值 {value} 小于最小值 {self.min_value}"
        if self.max_value is not None and value > self.max_value:
            return f"值 {value} 大于最大值 {self.max_value}"
        return None


class LengthValidator(Validator):
    """长度校验器"""

    def __init__(self, min_len: Optional[int] = None, max_len: Optional[int] = None):
        self.min_len = min_len
        self.max_len = max_len

    def validate(self, value: Any) -> Optional[str]:
        length = len(value)
        if self.min_len is not None and length < self.min_len:
            return f"长度 {length} 小于最小长度 {self.min_len}"
        if self.max_len is not None and length > self.max_len:
            return f"长度 {length} 大于最大长度 {self.max_len}"
        return None


class RegexValidator(Validator):
    """正则校验器"""

    def __init__(self, pattern: str, message: str = "格式不正确"):
        import re
        self.pattern = re.compile(pattern)
        self.message = message

    def validate(self, value: Any) -> Optional[str]:
        if not self.pattern.match(str(value)):
            return self.message
        return None


class ValidatedField:
    """
    生产级验证字段描述符
    
    输入参数:
        validators: 校验器列表
        default: 默认值
        nullable: 是否允许 None
        converter: 类型转换函数(可选)
    
    核心执行流程:
        1. __set__ 接收值
        2. 若配置 converter,先转换类型
        3. 依次执行所有校验器
        4. 任一校验失败,抛出 ValueError(含详细错误信息)
        5. 全部通过,存入实例 __dict__
    """

    def __init__(
        self,
        validators: Optional[List[Validator]] = None,
        default: Any = None,
        nullable: bool = True,
        converter: Optional[Callable] = None
    ):
        self.validators = validators or []
        self.default = default
        self.nullable = nullable
        self.converter = converter
        self.name: Optional[str] = None
        self._private_name: Optional[str] = None

    def __set_name__(self, owner: type, name: str) -> None:
        self.name = name
        self._private_name = f'_validated_{name}'

    def __get__(self, obj: Any, objtype: Optional[type] = None) -> Any:
        if obj is None:
            return self
        return getattr(obj, self._private_name, self.default)

    def __set__(self, obj: Any, value: Any) -> None:
        # None 处理
        if value is None:
            if not self.nullable:
                raise ValueError(ValidationError(
                    self.name, "不能为 None", "not_nullable"
                ).__str__())
            setattr(obj, self._private_name, None)
            return

        # 类型转换
        if self.converter is not None:
            try:
                value = self.converter(value)
            except (ValueError, TypeError) as e:
                raise ValueError(ValidationError(
                    self.name, f"类型转换失败: {e}", "convert_failed"
                ).__str__())

        # 执行校验器
        for validator in self.validators:
            error = validator(value)
            if error is not None:
                raise ValueError(ValidationError(
                    self.name, error, "validation_failed"
                ).__str__())

        # 通过校验,存储
        setattr(obj, self._private_name, value)


class UserForm:
    """用户表单,使用 ValidatedField 进行严格校验"""
    # 用户名:3-20 字符,仅字母数字下划线
    username = ValidatedField(
        validators=[
            LengthValidator(min_len=3, max_len=20),
            RegexValidator(r'^[a-zA-Z0-9_]+$', "用户名仅允许字母数字下划线")
        ],
        nullable=False
    )
    # 年龄:18-150 整数
    age = ValidatedField(
        validators=[RangeValidator(min_value=18, max_value=150)],
        default=18,
        converter=int
    )
    # 邮箱:标准格式
    email = ValidatedField(
        validators=[
            RegexValidator(r'^[\w.+-]+@[\w-]+\.[\w.]+$', "邮箱格式不正确")
        ],
        nullable=False
    )


# 使用示例
form = UserForm()
form.username = "zhang_san"
form.age = "25"  # 字符串会被 converter 转为 int
form.email = "zhang@example.com"
print(f"用户名: {form.username}, 年龄: {form.age}, 邮箱: {form.email}")

try:
    form.username = "ab"  # 长度不足
except ValueError as e:
    print(f"校验失败: {e}")

try:
    form.age = 200  # 超出范围
except ValueError as e:
    print(f"校验失败: {e}")

8.2 性能优化:避免 __get__ 中的重复计算

数据描述符的 __get__ 在每次访问时调用,若其中包含复杂逻辑,会显著影响性能。优化策略:

class OptimizedField:
    """优化后的字段:首次计算后转为实例属性,跳过描述符"""

    def __init__(self, compute_func: Callable):
        self.compute_func = compute_func
        self.name = compute_func.__name__
        self._private_name = f'_computed_{self.name}'

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        # 检查是否已计算
        if self._private_name in obj.__dict__:
            return obj.__dict__[self._private_name]
        # 计算并存入实例 __dict__
        value = self.compute_func(obj)
        obj.__dict__[self._private_name] = value
        return value

注意:此模式仅适用于只读属性。若需支持赋值,应保留 __set__ 并在赋值时清除 _private_name

8.3 与 dataclasses 协作

Python 3.7+ 的 dataclasses 与描述符可以协作,但需注意 dataclass 会在 __init__ 中赋值,触发描述符的 __set__

from dataclasses import dataclass

class PositiveInt:
    """正整数描述符"""
    def __set_name__(self, owner, name):
        self._private_name = f'_{name}'

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self._private_name, 0)

    def __set__(self, obj, value):
        if value <= 0:
            raise ValueError(f"必须为正整数, 实际 {value}")
        setattr(obj, self._private_name, value)


@dataclass
class Product:
    """产品类,dataclass 与描述符协作"""
    name: str
    price: PositiveInt = PositiveInt()  # 描述符作为默认值


p = Product("phone", 999)
print(p.price)  # 999

try:
    p.price = -1  # ValueError
except ValueError as e:
    print(e)

注意dataclass 默认会将描述符视为类属性,需在 field(default=...) 中传入描述符实例。更推荐的做法是使用 __post_init__ 进行校验。

8.4 与 Pydantic 风格的字段定义

参考 Pydantic v2 的设计,实现声明式字段定义:

from typing import Any, Optional, Type, get_type_hints

class FieldInfo:
    """字段元信息"""

    def __init__(
        self,
        default: Any = ...,
        description: str = "",
        alias: Optional[str] = None,
        min_value: Optional[float] = None,
        max_value: Optional[float] = None
    ):
        self.default = default
        self.description = description
        self.alias = alias
        self.min_value = min_value
        self.max_value = max_value
        self.name: Optional[str] = None
        self.type: Optional[Type] = None

    def __set_name__(self, owner: type, name: str) -> None:
        self.name = name
        # 从类型注解获取类型
        hints = get_type_hints(owner)
        self.type = hints.get(name, Any)

    def __get__(self, obj: Any, objtype: Optional[type] = None) -> Any:
        if obj is None:
            return self
        return getattr(obj, f'_field_{self.name}', self.default if self.default is not ... else None)

    def __set__(self, obj: Any, value: Any) -> None:
        # 类型检查
        if self.type is not None and not isinstance(value, self.type):
            raise TypeError(f"{self.name} 期望 {self.type.__name__}, 实际 {type(value).__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}")
        setattr(obj, f'_field_{self.name}', value)


class PydanticStyleModel:
    """Pydantic 风格的模型基类"""

    def __init__(self, **kwargs):
        # 收集所有 FieldInfo
        for name, value in type(self).__dict__.items():
            if isinstance(value, FieldInfo):
                # 设置默认值
                if value.default is not ...:
                    setattr(self, name, value.default)
        # 应用传入的值
        for key, value in kwargs.items():
            setattr(self, key, value)

    def to_dict(self) -> dict:
        """序列化"""
        result = {}
        for name, value in type(self).__dict__.items():
            if isinstance(value, FieldInfo):
                result[name] = getattr(self, name)
        return result


class UserConfig(PydanticStyleModel):
    """用户配置"""
    name: str = FieldInfo(default="", description="用户名")
    age: int = FieldInfo(default=18, min_value=0, max_value=150)
    email: str = FieldInfo(default="", description="邮箱")


user = UserConfig(name="张三", age=25, email="zhang@example.com")
print(user.to_dict())  # {'name': '张三', 'age': 25, 'email': 'zhang@example.com'}

8.5 描述符的单元测试

import unittest

class TestTypedField(unittest.TestCase):
    """TypedField 描述符单元测试"""

    def setUp(self):
        class TestClass:
            name = TypedField(str, default="", nullable=False)
            age = TypedField(int, default=0)

        self.TestClass = TestClass
        self.obj = TestClass()

    def test_default_value(self):
        """测试默认值"""
        self.assertEqual(self.obj.name, "")
        self.assertEqual(self.obj.age, 0)

    def test_valid_assignment(self):
        """测试合法赋值"""
        self.obj.name = "张三"
        self.obj.age = 25
        self.assertEqual(self.obj.name, "张三")
        self.assertEqual(self.obj.age, 25)

    def test_type_check(self):
        """测试类型检查"""
        with self.assertRaises(TypeError):
            self.obj.age = "25"

    def test_nullable_check(self):
        """测试非空约束"""
        with self.assertRaises(TypeError):
            self.obj.name = None

    def test_class_access(self):
        """测试类访问返回描述符"""
        self.assertIsInstance(self.TestClass.name, TypedField)


if __name__ == '__main__':
    unittest.main()

9. 案例研究

9.1 案例一:Django ORM 的描述符应用

Django ORM 使用描述符实现字段访问与外键关联。以 ForeignKey 为例:

# Django ORM 简化实现
class ForeignKeyDescriptor:
    """外键描述符:惰性加载关联对象"""

    def __init__(self, related_model: str, on_delete: str = 'CASCADE'):
        self.related_model = related_model
        self.on_delete = on_delete

    def __set_name__(self, owner, name):
        self.name = name
        self.attname = f'{name}_id'  # 数据库中存储外键 ID 的字段名

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        # 检查缓存
        if hasattr(obj, f'_cached_{self.name}'):
            return getattr(obj, f'_cached_{self.name}')
        # 从数据库加载关联对象
        related_id = getattr(obj, self.attname, None)
        if related_id is None:
            return None
        # 模拟数据库查询
        related_obj = self._load_related(related_id)
        # 缓存
        setattr(obj, f'_cached_{self.name}', related_obj)
        return related_obj

    def __set__(self, obj, value):
        # 设置关联对象
        if value is None:
            setattr(obj, self.attname, None)
        else:
            setattr(obj, self.attname, value.id)
            setattr(obj, f'_cached_{self.name}', value)

    def _load_related(self, related_id):
        """模拟数据库加载"""
        # 实际中调用 self.related_model.objects.get(id=related_id)
        return {'id': related_id, 'name': f'User_{related_id}'}


class Order:
    """订单模型"""
    # 外键:关联 User 模型
    user = ForeignKeyDescriptor(related_model='User')
    amount = TypedField(float, default=0.0)


# 使用示例
order = Order()
order.user = {'id': 1, 'name': '张三'}  # 设置关联对象
print(order.user)  # {'id': 1, 'name': '张三'}

# 直接设置外键 ID
order2 = Order()
setattr(order2, 'user_id', 2)
print(order2.user)  # {'id': 2, 'name': 'User_2'}(惰性加载)

分析:Django 的 ForeignKey 通过描述符实现了:(1)惰性加载——首次访问才查询数据库;(2)缓存——后续访问直接返回缓存;(3)外键 ID 与关联对象的统一管理。

9.2 案例二:Pydantic v2 的字段验证

Pydantic v2 使用描述符实现类型验证与序列化:

# Pydantic 简化实现
from typing import get_type_hints

class PydanticField:
    """Pydantic 风格字段"""

    def __init__(self, default=..., **constraints):
        self.default = default
        self.constraints = constraints

    def __set_name__(self, owner, name):
        self.name = name
        hints = get_type_hints(owner)
        self.type = hints.get(name, Any)

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, f'_pydantic_{self.name}', None)

    def __set__(self, obj, value):
        # 类型强制转换(Pydantic 的特性)
        value = self._coerce(value)
        # 约束校验
        self._validate_constraints(value)
        setattr(obj, f'_pydantic_{self.name}', value)

    def _coerce(self, value):
        """类型强制转换"""
        if self.type is int:
            return int(value)
        elif self.type is float:
            return float(value)
        elif self.type is str:
            return str(value)
        return value

    def _validate_constraints(self, value):
        """约束校验"""
        for key, constraint in self.constraints.items():
            if key == 'ge' and value < constraint:
                raise ValueError(f"{self.name} 必须 >= {constraint}")
            elif key == 'le' and value > constraint:
                raise ValueError(f"{self.name} 必须 <= {constraint}")
            elif key == 'min_length' and len(value) < constraint:
                raise ValueError(f"{self.name} 长度必须 >= {constraint}")


class BaseModel:
    """Pydantic 风格基类"""

    def __init__(self, **data):
        for name in get_type_hints(type(self)):
            field = getattr(type(self), name, None)
            if isinstance(field, PydanticField):
                if name in data:
                    setattr(self, name, data[name])
                elif field.default is not ...:
                    setattr(self, name, field.default)


class User(BaseModel):
    """用户模型"""
    name: str = PydanticField(default="", min_length=2)
    age: int = PydanticField(default=18, ge=0, le=150)
    email: str = PydanticField(default="")


user = User(name="张三", age="25", email="zhang@example.com")
print(f"姓名: {user.name}, 年龄: {user.age}")  # 年龄被强制转换为 int

9.3 案例三:缓存属性在生产中的应用

某数据分析平台使用 CachedProperty 缓存数据预处理结果,将重复计算时间从 30 秒降至 0.1 秒:

import time

class DataAnalyzer:
    """数据分析器"""

    def __init__(self, raw_data: list):
        self.raw_data = raw_data

    @CachedProperty
    def cleaned_data(self) -> list:
        """数据清洗(耗时 5 秒)"""
        print("执行数据清洗...")
        time.sleep(5)
        return [x for x in self.raw_data if x is not None]

    @CachedProperty
    def normalized_data(self) -> list:
        """数据归一化(依赖清洗结果,耗时 10 秒)"""
        print("执行数据归一化...")
        time.sleep(10)
        max_val = max(self.cleaned_data)  # 复用缓存
        return [x / max_val for x in self.cleaned_data]

    @CachedProperty
    def statistics(self) -> dict:
        """统计信息(依赖归一化结果,耗时 15 秒)"""
        print("计算统计信息...")
        time.sleep(15)
        data = self.normalized_data  # 复用缓存
        return {
            'mean': sum(data) / len(data),
            'max': max(data),
            'min': min(data),
        }


# 首次访问:30 秒
analyzer = DataAnalyzer([1, 2, 3, 4, 5])
start = time.time()
stats = analyzer.statistics
print(f"首次访问耗时: {time.time() - start:.1f}s")

# 二次访问:0 秒(全部命中缓存)
start = time.time()
stats = analyzer.statistics
print(f"二次访问耗时: {time.time() - start:.1f}s")

分析CachedProperty 通过非数据描述符特性,首次计算后存入实例 __dict__,后续访问直接读取实例属性,跳过描述符 __get__,实现 O(1) 缓存命中。多个 CachedProperty 之间通过实例属性传递结果,避免重复计算。

9.4 案例四:使用描述符实现领域特定语言(DSL)

某配置系统使用描述符实现声明式 DSL,让用户以类属性方式定义配置:

class ConfigOption:
    """配置选项描述符"""

    def __init__(
        self,
        default=None,
        env_var: str = None,
        description: str = "",
        converter: Callable = None
    ):
        self.default = default
        self.env_var = env_var
        self.description = description
        self.converter = converter or (lambda x: x)

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

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        # 优先级:环境变量 > 配置文件 > 默认值
        import os
        if self.env_var and self.env_var in os.environ:
            value = os.environ[self.env_var]
        elif hasattr(obj, f'_config_{self.name}'):
            value = getattr(obj, f'_config_{self.name}')
        else:
            value = self.default
        return self.converter(value)

    def __set__(self, obj, value):
        setattr(obj, f'_config_{self.name}', value)


class AppConfig:
    """应用配置,声明式定义"""

    # 数据库配置
    db_url = ConfigOption(
        default="sqlite:///default.db",
        env_var="DATABASE_URL",
        description="数据库连接地址"
    )
    db_pool_size = ConfigOption(
        default=10,
        env_var="DB_POOL_SIZE",
        description="数据库连接池大小",
        converter=int
    )

    # Redis 配置
    redis_url = ConfigOption(
        default="redis://localhost:6379/0",
        env_var="REDIS_URL",
        description="Redis 连接地址"
    )

    # 应用配置
    debug = ConfigOption(
        default=False,
        env_var="DEBUG",
        description="调试模式",
        converter=lambda x: str(x).lower() in ('true', '1', 'yes')
    )
    port = ConfigOption(
        default=8000,
        env_var="PORT",
        description="监听端口",
        converter=int
    )


# 使用示例
import os
os.environ['DEBUG'] = 'true'
os.environ['PORT'] = '9000'

config = AppConfig()
print(f"数据库: {config.db_url}")       # sqlite:///default.db
print(f"连接池: {config.db_pool_size}")  # 10
print(f"调试模式: {config.debug}")       # True(从环境变量读取)
print(f"端口: {config.port}")            # 9000(从环境变量读取)

# 也可运行时覆盖
config.db_url = "postgresql://localhost/mydb"
print(f"数据库(覆盖后): {config.db_url}")  # postgresql://localhost/mydb

10. 习题

10.1 基础题

题目 1:编写一个 EmailField 描述符,要求:(1)必须为字符串;(2)符合基本邮箱格式(含 @.);(3)不满足时抛出 ValueError

参考答案要点

  • 使用 isinstance(value, str) 检查类型
  • 使用正则 ^[\w.+-]+@[\w-]+\.[\w.]+$ 校验格式
  • __set_name__ 自动获取字段名
  • 数据存入实例 __dict__ 的私有属性

题目 2:解释以下代码的输出,并说明原因:

class D:
    def __get__(self, obj, objtype=None):
        return "descriptor"

class C:
    d = D()
    def __init__(self):
        self.d = "instance"

c = C()
print(c.d)

参考答案要点

  • 输出 "instance"
  • 原因:D 仅实现 __get__,是非数据描述符
  • 非数据描述符优先级低于实例属性
  • __init__self.d = "instance" 写入实例 __dict__,遮蔽了描述符

题目 3:将 property 装饰器转换为等价的描述符类实现。

参考答案要点

  • 定义 __init__ 接收 fgetfsetfdel
  • 实现 __get__ 调用 fget(obj)
  • 实现 __set__ 调用 fset(obj, value)
  • 实现 __delete__ 调用 fdel(obj)
  • 提供 getter/setter/deleter 方法返回新实例

10.2 进阶题

题目 4:实现一个 LazyForeignKey 描述符,模拟 ORM 外键:(1)赋值时存储 ID;(2)访问时若未缓存,从”数据库”加载关联对象;(3)支持 del 清除缓存。

参考答案要点

  • 实现 __set__ 存储 ID 到 obj._fk_{name}_id
  • 实现 __get__ 检查 obj._fk_{name}_cache,未命中则加载
  • 实现 __delete__ 清除缓存(不清除 ID)
  • 使用数据描述符确保优先级

题目 5:在 __slots__ 类中使用描述符,要求每个实例独立持有数据,且实例销毁时自动清理。

参考答案要点

  • 使用 weakref.WeakKeyDictionary 存储数据
  • __slots__ 中包含 '__weakref__'
  • __get__WeakKeyDictionary 读取
  • __set__ 写入 WeakKeyDictionary

题目 6:分析以下代码为何内存泄漏,并给出修复方案:

class Cache:
    _data = {}
    def __get__(self, obj, objtype=None):
        if obj not in self._data:
            self._data[obj] = compute(obj)
        return self._data[obj]

参考答案要点

  • 泄漏原因:_data 是普通 dict,持有 obj 的强引用,阻止 GC
  • 修复:改用 weakref.WeakKeyDictionary
  • 实例销毁时 WeakKeyDictionary 自动清理对应条目

10.3 挑战题

题目 7:设计一个支持字段继承的 ORM 基类,要求:(1)子类自动继承父类字段;(2)字段顺序保持定义顺序;(3)支持 to_dict()from_dict() 序列化;(4)支持类型注解校验。

参考答案要点

  • 使用元类 ModelMeta 收集字段,合并父类字段
  • 使用 dict 保持插入顺序(Python 3.7+)
  • to_dict() 遍历 _fields 序列化
  • from_dict() 反序列化并校验类型
  • 字段使用 FieldInfo 描述符,存储类型注解

题目 8:实现一个支持事务的属性描述符:批量赋值时,要么全部成功,要么全部回滚。

参考答案要点

  • 使用上下文管理器记录旧值
  • __enter__ 备份当前值
  • __exit__ 异常时恢复旧值
  • 描述符 __set__ 在事务中暂存修改
  • 提交时应用,回滚时恢复

题目 9:分析 Python 标准库 functools.cached_property 的实现,与本文的 CachedProperty 对比,指出差异与优化点。

参考答案要点

  • functools.cached_property 是非数据描述符
  • 使用 __dict__ 存储,与本文一致
  • 增加了线程锁(RLock)保证线程安全
  • 异常时不缓存(计算抛异常则不写入 __dict__
  • 优化点:__set_name__ 缓存属性名;__get__ 使用 try/except KeyError 优化

11. 参考文献

[1] Guido van Rossum. 2002. Unifying types and classes in Python 2.2. Python Enhancement Proposal 253. Retrieved July 21, 2026, from https://peps.python.org/pep-0253/

[2] Paul F. Dubois. 2001. PEP 252: Make types look more like classes. Python Enhancement Proposal 252. Retrieved July 21, 2026, from https://peps.python.org/pep-0252/

[3] Martin von Löwis. 2017. PEP 487: Simpler customisation of class creation. Python Enhancement Proposal 487. DOI: 10.5281/zenodo.1154727. Retrieved July 21, 2026, from https://peps.python.org/pep-0487/

[4] Raymond Hettinger. 2002. Python Descriptor HowTo Guide. Python Official Documentation. Retrieved July 21, 2026, from https://docs.python.org/3/howto/descriptor.html

[5] Python Software Foundation. 2024. The Python Language Reference: 3.3.2. Customizing class creation. Retrieved July 21, 2026, from https://docs.python.org/3/reference/datamodel.html#customizing-class-creation

[6] Python Software Foundation. 2024. The Python Language Reference: Invoking descriptors. Retrieved July 21, 2026, from https://docs.python.org/3/reference/datamodel.html#invoking-descriptors

[7] Gregor J. Wardle. 2010. PEP 3115: Metaclass syntax in Python 3000. Python Enhancement Proposal 3115. Retrieved July 21, 2026, from https://peps.python.org/pep-3115/

[8] Eric Snow. 2019. PEP 520: Preserving class attribute definition order. Python Enhancement Proposal 520. Retrieved July 21, 2026, from https://peps.python.org/pep-0520/

[9] Brett Cannon. 2017. PEP 557: Dataclasses. Python Enhancement Proposal 557. DOI: 10.5281/zenodo.1154733. Retrieved July 21, 2026, from https://peps.python.org/pep-0557/

[10] Django Software Foundation. 2024. Django Documentation: Models and databases. Retrieved July 21, 2026, from https://docs.djangoproject.com/en/5.0/topics/db/models/

[11] SQLAlchemy Project. 2024. SQLAlchemy Documentation: Descriptors. Retrieved July 21, 2026, from https://docs.sqlalchemy.org/en/20/orm/descriptors.html

[12] Samuel Colvin. 2024. Pydantic Documentation: Fields. Retrieved July 21, 2026, from https://docs.pydantic.dev/latest/concepts/fields/

[13] Alex Martelli. 2013. Python in a Nutshell (3rd ed.). O’Reilly Media, Sebastopol, CA, USA. DOI: 10.5555/2571665

[14] David Beazley and Brian K. Jones. 2013. Python Cookbook (3rd ed.). O’Reilly Media, Sebastopol, CA, USA. Chapter 8: Classes and Objects. DOI: 10.5555/2502259

[15] Luciano Ramalho. 2022. Fluent Python (2nd ed.). O’Reilly Media, Sebastopol, CA, USA. Chapter 22: Attribute Descriptors. DOI: 10.5555/3564953

[16] Mark Lutz. 2013. Learning Python (5th ed.). O’Reilly Media, Sebastopol, CA, USA. Chapter 38: Managed Attributes.

12. 延伸阅读

12.1 官方文档

12.2 经典教材

  • Luciano Ramalho. Fluent Python (2nd ed.), Chapter 22: Attribute Descriptors
  • David Beazley & Brian Jones. Python Cookbook (3rd ed.), Chapter 8: Classes and Objects
  • Mark Lutz. Learning Python (5th ed.), Chapter 38: Managed Attributes
  • Alex Martelli et al. Python in a Nutshell (3rd ed.)

12.3 框架源码

  • Django ModelBase 与 Field 实现: django/db/models/base.py, django/db/models/fields/__init__.py
  • SQLAlchemy descriptors 模块: lib/sqlalchemy/orm/descriptors.py
  • Pydantic v2 字段实现: pydantic/fields.py
  • attrs 库的 Attribute 与 define: attr/_make.py

12.4 前沿论文与讨论

  • PEP 487: Simpler customisation of class creation — __set_name__ 的引入动机
  • PEP 520: Preserving class attribute definition order — 对描述符收集的影响
  • PEP 557: Dataclasses — 与描述符的协作模式
  • Python-ideas 邮件列表中关于”descriptor enhancements”的讨论

12.5 相关主题

  • python/元类: 元类与描述符的协作
  • python/装饰器: 装饰器与描述符的等价关系(property 即装饰器即描述符)
  • python/类与对象: 新式类与 MRO
  • python/Python与设计模式: 描述符实现的设计模式(观察者、代理、单例)
返回入门指南