Python 属性描述符:从原理到 ORM 实践详解

Python 属性描述符(Descriptor) 是 Python 面向对象中最强大、最被低估的核心机制之一。它被誉为“Python 元编程的基石”,直接支撑了:

  • @property@classmethod@staticmethod
  • 函数绑定为方法(bound method)
  • Django ORM / SQLAlchemy / Peewee 等所有主流 ORM 的字段(Field/Column)
  • 类型验证、懒加载、缓存、代理、数据校验等高级特性

掌握描述符,你就真正打开了“理解 Python 黑魔法”的大门。

下面从零基础原理 → 底层机制 → 自定义实战 → ORM 深度应用(基于 Python 3.12~3.14 最新特性,2026 年 3 月视角)进行系统详解。

1. 什么是描述符?一句话定义

描述符就是一个实现了描述符协议的对象,当它作为类属性存在时,会接管该属性在实例上的 get/set/delete 操作。

描述符协议(Descriptor Protocol)只有 4 个特殊方法:

class Descriptor:
    def __get__(self, instance, owner=None):
        """instance 是实例,owner 是类"""
        ...

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

    def __delete__(self, instance):
        ...

    def __set_name__(self, owner, name):   # Python 3.6+
        """自动记录属性名,超级好用"""
        self.name = name

只要实现了 __get__ / __set__ / __delete__ 中的至少一个,就是一个描述符。

2. 数据描述符 vs 非数据描述符(最核心区别!)

类型必须实现的方法优先级(相对于实例 dict典型例子
数据描述符__get__ + __set__(或 __delete__高于实例字典@property、ORM Field
非数据描述符只实现 __get__低于实例字典函数、classmethod、staticmethod

关键规则(背下来)

  • 数据描述符优先级最高:即使实例 __dict__ 中有同名键,也走描述符的 __get__ / __set__
  • 非数据描述符优先级最低:实例有同名属性时,直接返回实例的,不会调用描述符

这也是为什么 @property 可以“覆盖”实例属性,而普通方法不行。

3. 属性查找完整流程(getattribute 机制)

当你执行 obj.attr 时,Python 实际调用的是 type(obj).__getattribute__(obj, 'attr'),其查找顺序(极简版):

  1. 在类和基类中查找 attr
  2. 如果找到的是数据描述符 → 立即调用 desc.__get__(obj, type(obj))
  3. 如果找到的是非数据描述符 或 普通属性 → 先检查 obj.__dict__
  4. 实例 __dict__ 有 → 返回
  5. 否则调用非数据描述符的 __get__
  6. 再找 __getattr__(兜底)

这套机制让描述符成为“属性劫持神器”。

4. 内置描述符拆解

4.1 @property(最常用数据描述符)

class Person:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not value:
            raise ValueError("名字不能为空")
        self._name = value

底层其实是:

name = property(fget=..., fset=..., fdel=..., doc=...)
# property 类实现了 __get__、__set__、__delete__

4.2 classmethod / staticmethod(非数据描述符)

它们只实现了 __get__,所以实例可以“覆盖”它们(极少见)。

4.3 函数本身也是描述符!

def func(self): pass
print(func.__get__(None, Person))   # <function func at ...>  → unbound
print(func.__get__(Person(), Person))  # <bound method ...>

这正是“实例调用方法自动传 self”的秘密。

5. 自定义描述符实战(从简单到高级)

示例1:类型+范围验证器(最常见)

class Typed:
    def __set_name__(self, owner, name):
        self.name = name          # Python 3.6+ 神器

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

    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(f"{self.name} 必须是 {self.expected_type}")
        if self.validator and not self.validator(value):
            raise ValueError(f"{self.name} 校验失败")
        instance.__dict__[self.name] = value   # 存到实例字典

class PositiveInt(Typed):
    expected_type = int
    validator = lambda x: x > 0

class User:
    age = PositiveInt()
    score = PositiveInt()

u = User()
u.age = 18     # OK
u.age = -5     # TypeError / ValueError

示例2:懒加载描述符(数据库/大文件场景)

class LazyAttribute:
    def __init__(self, func):
        self.func = func
        self.__name__ = func.__name__

    def __get__(self, instance, owner):
        if instance is None:
            return self
        value = self.func(instance)
        instance.__dict__[self.__name__] = value   # 缓存到实例
        return value

class Article:
    @LazyAttribute
    def content(self):
        print("从数据库加载大文本...")
        return "很长的文章内容..." * 1000

示例3:缓存描述符(带过期时间)

可扩展为 Redis 后端缓存。

6. ORM 实践:描述符如何驱动整个模型层

Django ORM(经典实现)

class ModelBase(type):
    def __new__(mcs, name, bases, attrs):
        for key, value in attrs.items():
            if isinstance(value, Field):   # Field 是数据描述符
                value.set_name(name, key)  # 记录字段名
        return super().__new__(mcs, name, bases, attrs)

class Field:
    def __set_name__(self, owner, name):
        self.attname = name          # 数据库列名

    def __get__(self, instance, owner):
        if instance is None:
            return self              # 类访问返回 Field 本身(用于 QuerySet)
        return instance.__dict__[self.attname]

    def __set__(self, instance, value):
        # 类型转换、校验、信号触发等
        instance.__dict__[self.attname] = value

class CharField(Field):
    def __init__(self, max_length):
        self.max_length = max_length

当你写:

class User(models.Model):
    name = models.CharField(max_length=100)

name 其实是一个 CharField 实例(数据描述符)!

访问 user.name → 走 __get__
user.name = "重阳" → 走 __set__(校验长度、触发 pre_save 等)

SQLAlchemy 2.0+(更现代)

class Column:
    def __set_name__(self, owner, name):
        ...

    # InstrumentedAttribute 是真正的描述符
    # 通过 __get__/__set__ 实现与 Session、查询、事件系统的深度集成

SQLAlchemy 使用了更复杂的 InstrumentedAttribute + QueryableAttribute,但核心仍是描述符协议。

7. 高级技巧与注意事项(2026 生产必知)

  1. set_name 是救命稻草
    Python 3.6+ 引入,自动传递属性名,彻底解决“硬编码属性名”的痛点。
  2. 描述符 + metaclass 是框架标配
    ORM 都在 metaclass 中收集所有 Field 描述符。
  3. 槽位类(slots)与描述符
    __slots__ 会阻止实例 __dict__ 创建,数据描述符仍可正常工作。
  4. 常见陷阱
  • 把描述符实例放在实例属性而不是类属性 → 失效
  • 非数据描述符被实例属性覆盖后永久失效
  • 多继承时描述符查找顺序(MRO)
  • 线程安全(描述符本身无锁,需自行处理)
  1. 性能
    数据描述符每次访问都会走 Python 方法调用,比普通属性慢 10~30%,但 ORM 场景完全可接受。

8. 一张图总结(文字版)

实例.attr
    ↓
type(实例).__getattribute__
    ↓
找到类属性是描述符?
├── 数据描述符 → 直接调用 __get__/__set__(绕过实例dict)
└── 非数据描述符 → 先查实例.__dict__,没有再调用 __get__

最后总结口诀

  • 想“劫持”属性访问 → 用描述符
  • @property 只是语法糖,底层就是描述符
  • ORM 的 Field/Column 本质都是描述符
  • 自定义描述符 = 复用 + 声明式编程 + 框架级能力

掌握描述符后,你再看 Django/SQLAlchemy 的源码会瞬间通透,也能自己写出优雅的配置类、验证框架、缓存系统。

推荐深入阅读(2026 最新)

  • 官方文档:https://docs.python.org/3/howto/descriptor.html
  • 《Fluent Python》 第 20 章(描述符章节极佳)
  • SQLAlchemy / Django 源码中的 fields.pyattributes.py

你现在最想看哪一部分的完整可运行代码?

  • 手写一个迷你 ORM(支持查询、校验、迁移)
  • SQLAlchemy 描述符源码拆解
  • 描述符实现 FastAPI Pydantic-like 自动验证
  • 线程安全的缓存描述符

告诉我,我立刻给你对应完整项目级代码!重阳,继续冲!描述符一旦掌握,Python 进阶之路就真正打开了。🚀

文章已创建 4915

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部