从 Python 描述符看 ORM 的魔法:字段映射、校验、延迟加载与查询构建

从 Python 描述符看 ORM 的魔法:字段映射、校验、延迟加载与查询构建

很多 Python 初学者第一次使用 ORM 时,都会有一种“它怎么知道我要查数据库”的惊讶感:

user = User.objects.get(id=1)
print(user.name)

User.objects.filter(age__gt=18)

看起来我们只是在访问普通属性、写普通 Python 表达式,但背后却可能发生了字段校验、类型转换、SQL 生成、缓存读取、延迟加载、脏数据追踪等一系列动作。

这些“魔法”的核心之一,就是 Python 的描述符机制

描述符并不是 ORM 的全部,但它是很多 ORM 框架设计模型字段时的重要基础。理解描述符,你就能更深刻地理解为什么 user.name 不只是一个字符串,为什么 User.name == "Alice" 可以变成 SQL 条件,为什么 ORM 能够在属性赋值时自动做校验和状态追踪。

这篇文章将从 Python 描述符的基础讲起,逐步拆解它在 ORM 中的典型用法,并手写一个极简 ORM 字段系统,帮助你真正理解这套机制。


一、描述符是什么?

在 Python 中,只要一个对象实现了以下任意方法,它就可以被称为描述符:

__get__(self, instance, owner)
__set__(self, instance, value)
__delete__(self, instance)

描述符通常作为类属性存在,用来接管实例属性的访问、赋值或删除行为。

一个最简单的描述符如下:

class Field:
    def __get__(self, instance, owner):
        print("读取字段")
        return instance.__dict__.get("value")

    def __set__(self, instance, value):
        print("设置字段")
        instance.__dict__["value"] = value


class User:
    name = Field()


u = User()
u.name = "Alice"
print(u.name)

输出结果大致是:

设置字段
读取字段
Alice

注意,u.name = "Alice" 并没有直接把 "Alice" 放到 u.__dict__["name"] 中,而是触发了 Field.__set__()

同理,u.name 也不是简单读取实例字典,而是触发了 Field.__get__()

这就是描述符的威力:它让普通属性访问变成了可编程的行为入口。


二、为什么 ORM 特别需要描述符?

ORM,全称 Object Relational Mapping,即对象关系映射。它的目标是把数据库表映射成 Python 类,把数据库记录映射成 Python 对象。

例如:

class User(Model):
    id = IntegerField(primary_key=True)
    name = StringField(max_length=50)
    age = IntegerField()

这段代码表达的意思是:

Python 类数据库
Userusers
idid 字段
namename 字段
ageage 字段
User(...) 实例表中的一行记录

问题来了:name = StringField(...) 是类属性,但我们希望在实例上这样使用:

user = User(name="Alice", age=20)
print(user.name)
user.age = 21

ORM 需要在这些看似普通的属性操作中完成很多任务:

  1. 字段名绑定;
  2. 类型检查;
  3. 默认值处理;
  4. 数据校验;
  5. 数据库存取映射;
  6. 延迟加载;
  7. 脏字段追踪;
  8. 查询表达式构建。

而描述符正好可以接管属性访问和赋值,因此非常适合做 ORM 字段系统的基础设施。


三、ORM 中描述符的第一种用法:字段映射

最基础的 ORM 字段,就是把模型属性和实例内部数据绑定起来。

我们先实现一个简单字段描述符:

class Field:
    def __init__(self, column_name=None, default=None):
        self.column_name = column_name
        self.default = default
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name
        if self.column_name is None:
            self.column_name = name

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

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

这里有一个非常关键的方法:__set_name__()

它会在类创建时自动调用,告诉描述符自己被赋值给了哪个类属性。

例如:

class User:
    name = Field()

User 类创建完成时,Python 会自动调用:

User.name.__set_name__(User, "name")

这样 Field 就知道自己对应的字段名是 "name"

测试一下:

class User:
    name = Field(default="unknown")
    age = Field(default=0)


u = User()
print(u.name)
u.name = "Alice"
print(u.name)

输出:

unknown
Alice

到这里,我们已经有了 ORM 字段的雏形:类上声明字段,实例上读写数据。


四、ORM 中描述符的第二种用法:类型校验与数据清洗

真实项目中,数据库字段通常有明确类型。例如用户名必须是字符串,年龄必须是整数,邮箱必须符合格式。

我们可以让不同字段类继承 Field,在赋值时执行校验。

class ValidationError(Exception):
    pass


class Field:
    def __init__(self, column_name=None, default=None, nullable=True):
        self.column_name = column_name
        self.default = default
        self.nullable = nullable
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name
        if self.column_name is None:
            self.column_name = name

    def validate(self, value):
        if value is None and not self.nullable:
            raise ValidationError(f"{self.name} 不能为空")
        return value

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

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


class StringField(Field):
    def __init__(self, max_length=None, **kwargs):
        super().__init__(**kwargs)
        self.max_length = max_length

    def validate(self, value):
        value = super().validate(value)
        if value is None:
            return value
        if not isinstance(value, str):
            raise ValidationError(f"{self.name} 必须是字符串")
        if self.max_length and len(value) > self.max_length:
            raise ValidationError(f"{self.name} 长度不能超过 {self.max_length}")
        return value


class IntegerField(Field):
    def validate(self, value):
        value = super().validate(value)
        if value is None:
            return value
        if not isinstance(value, int):
            raise ValidationError(f"{self.name} 必须是整数")
        return value

使用方式如下:

class User:
    name = StringField(max_length=20, nullable=False)
    age = IntegerField(default=0)


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

print(u.name, u.age)

u.age = "18"

最后一行会抛出异常:

ValidationError: age 必须是整数

这就是 ORM 字段描述符最常见的用途之一:让属性赋值自动具备校验能力。

从使用者角度看,代码仍然是自然的:

user.age = 18

但框架内部已经完成了类型检查、空值判断和错误提示。

好的 ORM 设计,往往就是把复杂规则藏在自然语义后面。


五、ORM 中描述符的第三种用法:脏字段追踪

ORM 不仅要知道字段的当前值,还要知道哪些字段被修改过。

例如:

user = User.get(id=1)
user.name = "Bob"
user.save()

执行 save() 时,ORM 可以只更新被修改过的字段:

UPDATE users SET name = 'Bob' WHERE id = 1;

而不是把整行数据全部更新一遍。

这就需要“脏字段追踪”。描述符可以在 __set__() 中记录修改状态。

class Field:
    def __init__(self, column_name=None, default=None):
        self.column_name = column_name
        self.default = default
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name
        if self.column_name is None:
            self.column_name = name

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

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

        if old_value != value:
            instance._dirty_fields.add(self.name)

配合一个基础模型类:

class Model:
    def __init__(self, **kwargs):
        self._dirty_fields = set()
        for key, value in kwargs.items():
            setattr(self, key, value)
        self._dirty_fields.clear()

    def get_dirty_fields(self):
        return self._dirty_fields


class User(Model):
    name = Field()
    age = Field()


user = User(name="Alice", age=20)

print(user.get_dirty_fields())

user.age = 21
print(user.get_dirty_fields())

输出:

set()
{'age'}

初始化时传入的字段不算修改,因为它们通常代表从数据库加载出来的初始状态。

当后续设置 user.age = 21 时,描述符自动记录 age 发生变化。

这类机制在真实 ORM 中非常重要。它可以减少数据库写入压力,也能让业务系统更容易实现审计日志、数据同步、事件通知等功能。


六、ORM 中描述符的第四种用法:延迟加载

延迟加载,也叫 lazy loading,是 ORM 中非常经典的技术。

假设一篇文章有一个作者:

article.author

如果文章数据已经加载,但作者信息还没有加载,那么 ORM 可以等到真正访问 article.author 时,再去数据库查询作者。

描述符非常适合实现这种行为,因为它能拦截读取操作。

class LazyForeignKey:
    def __init__(self, model_class, fk_name):
        self.model_class = model_class
        self.fk_name = fk_name
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name
        self.cache_name = f"_{name}_cache"

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

        if hasattr(instance, self.cache_name):
            return getattr(instance, self.cache_name)

        fk_value = getattr(instance, self.fk_name)
        related_obj = self.model_class.get(id=fk_value)

        setattr(instance, self.cache_name, related_obj)
        return related_obj

    def __set__(self, instance, value):
        setattr(instance, self.cache_name, value)
        setattr(instance, self.fk_name, value.id)

示意用法:

class User(Model):
    id = Field()
    name = Field()

    @classmethod
    def get(cls, id):
        print(f"查询用户表,id={id}")
        return cls(id=id, name="Alice")


class Article(Model):
    title = Field()
    author_id = Field()
    author = LazyForeignKey(User, "author_id")


article = Article(title="Python 描述符", author_id=1)

print(article.title)
print(article.author.name)
print(article.author.name)

可能输出:

Python 描述符
查询用户表,id=1
Alice
Alice

第一次访问 article.author 时发生数据库查询,第二次访问时直接读取缓存。

这就是延迟加载的核心思想:

对象创建时不加载关联数据
        ↓
访问关联属性时触发描述符
        ↓
根据外键查询数据库
        ↓
缓存结果,避免重复查询

当然,真实 ORM 会更加复杂:它需要处理连接池、事务、批量预加载、循环引用、缓存失效等问题。但底层思想并不神秘,关键入口仍然是属性访问。


七、ORM 中描述符的第五种用法:查询表达式构建

描述符还有一个更高级、更迷人的用途:在类上访问字段时,返回查询表达式对象。

注意下面两种访问方式的区别:

user.name
User.name

当通过实例访问时,instance 不为 None

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

当通过类访问时,instanceNone

这给 ORM 留下了一个非常优雅的设计空间:

  • user.name 返回真实字段值;
  • User.name 返回字段表达式,用于构建 SQL 查询。

例如:

class QueryExpression:
    def __init__(self, field, operator, value):
        self.field = field
        self.operator = operator
        self.value = value

    def to_sql(self):
        return f"{self.field.column_name} {self.operator} {repr(self.value)}"


class FieldExpression:
    def __init__(self, field):
        self.field = field

    def __eq__(self, other):
        return QueryExpression(self.field, "=", other)

    def __gt__(self, other):
        return QueryExpression(self.field, ">", other)

    def __lt__(self, other):
        return QueryExpression(self.field, "<", other)


class Field:
    def __init__(self, column_name=None):
        self.column_name = column_name
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name
        if self.column_name is None:
            self.column_name = name

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

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

使用:

class User:
    name = Field()
    age = Field()


expr = User.age > 18
print(expr.to_sql())

输出:

age > 18

这就是为什么某些 ORM 可以写出类似这样的代码:

query = User.age > 18

看起来像 Python 比较运算,实际上是在构建 SQL 表达式树。

这类设计非常适合构建类型安全、可组合、可读性强的查询接口。


八、手写一个极简 ORM 示例

下面我们把前面的能力组合起来,实现一个极简模型系统。

它支持:

  1. 字段声明;
  2. 字段校验;
  3. 脏字段追踪;
  4. SQL 插入语句生成;
  5. SQL 更新语句生成。
class ValidationError(Exception):
    pass


class Field:
    python_type = object

    def __init__(self, column_name=None, default=None, nullable=True, primary_key=False):
        self.column_name = column_name
        self.default = default
        self.nullable = nullable
        self.primary_key = primary_key
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name
        if self.column_name is None:
            self.column_name = name

    def validate(self, value):
        if value is None:
            if not self.nullable:
                raise ValidationError(f"{self.name} 不能为空")
            return value

        if self.python_type is not object and not isinstance(value, self.python_type):
            raise ValidationError(f"{self.name} 必须是 {self.python_type.__name__}")

        return value

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

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

        old_value = instance.__dict__.get(self.name, self.default)
        instance.__dict__[self.name] = value

        if hasattr(instance, "_dirty_fields") and old_value != value:
            instance._dirty_fields.add(self.name)


class IntegerField(Field):
    python_type = int


class StringField(Field):
    python_type = str

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

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

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

        return value

接着定义元类,用于收集模型字段:

class ModelMeta(type):
    def __new__(mcls, name, bases, namespace):
        cls = super().__new__(mcls, name, bases, namespace)

        fields = {}
        for base in reversed(cls.__mro__):
            for attr_name, attr_value in base.__dict__.items():
                if isinstance(attr_value, Field):
                    fields[attr_name] = attr_value

        cls._fields = fields

        if not hasattr(cls, "__tablename__"):
            cls.__tablename__ = name.lower()

        return cls

基础模型类:

class Model(metaclass=ModelMeta):
    __tablename__ = None

    def __init__(self, **kwargs):
        self._dirty_fields = set()

        for field_name, field in self._fields.items():
            value = kwargs.get(field_name, field.default)
            setattr(self, field_name, value)

        self._dirty_fields.clear()

    @classmethod
    def fields(cls):
        return cls._fields

    def to_dict(self):
        return {
            name: getattr(self, name)
            for name in self._fields
        }

    def insert_sql(self):
        columns = []
        values = []

        for name, field in self._fields.items():
            value = getattr(self, name)
            if value is not None:
                columns.append(field.column_name)
                values.append(repr(value))

        columns_part = ", ".join(columns)
        values_part = ", ".join(values)

        return f"INSERT INTO {self.__tablename__} ({columns_part}) VALUES ({values_part});"

    def update_sql(self):
        sets = []

        primary_key_name = None
        primary_key_value = None

        for name, field in self._fields.items():
            value = getattr(self, name)

            if field.primary_key:
                primary_key_name = field.column_name
                primary_key_value = value
                continue

            if name in self._dirty_fields:
                sets.append(f"{field.column_name} = {repr(value)}")

        if not primary_key_name:
            raise RuntimeError("缺少主键字段")

        if not sets:
            return "-- 没有字段需要更新"

        set_part = ", ".join(sets)
        return (
            f"UPDATE {self.__tablename__} "
            f"SET {set_part} "
            f"WHERE {primary_key_name} = {repr(primary_key_value)};"
        )

定义模型:

class User(Model):
    __tablename__ = "users"

    id = IntegerField(primary_key=True, nullable=False)
    name = StringField(max_length=30, nullable=False)
    age = IntegerField(default=0)

使用:

user = User(id=1, name="Alice", age=20)

print(user.to_dict())
print(user.insert_sql())

user.age = 21
print(user.update_sql())

输出:

{'id': 1, 'name': 'Alice', 'age': 20}
INSERT INTO users (id, name, age) VALUES (1, 'Alice', 20);
UPDATE users SET age = 21 WHERE id = 1;

这个示例虽然非常简化,却已经覆盖了 ORM 字段系统的核心思想。

在真实项目中,SQL 生成必须使用参数化查询,不能直接拼接用户输入,否则会有 SQL 注入风险。上面的代码只用于理解描述符与 ORM 的关系,不建议直接用于生产环境。


九、描述符、元类和 ORM 的协作关系

描述符通常负责“单个字段”的行为,元类通常负责“整个模型类”的组织。

可以这样理解:

描述符 Field
    负责字段读写、校验、延迟加载、脏字段追踪

元类 ModelMeta
    负责收集字段、生成元数据、绑定表名、构建模型结构

模型基类 Model
    负责初始化、保存、查询、序列化等通用行为

它们协作起来,就形成了 ORM 的基础骨架。

一个简化的结构示意图如下:

+----------------+
|    ModelMeta   |
+----------------+
        |
        | 创建类时收集字段
        v
+----------------+
|     User       |
|----------------|
| id = Field     |
| name = Field   |
| age = Field    |
+----------------+
        |
        | 实例化
        v
+----------------+
|   user object  |
|----------------|
| __dict__       |
| _dirty_fields  |
+----------------+
        ^
        |
        | 属性读写触发
+----------------+
|   Descriptor   |
| __get__/__set__|
+----------------+

这也是 Python 语言非常迷人的地方:它允许框架开发者利用语言协议设计出近乎自然语言的接口。


十、实践中的最佳建议

1. 描述符适合封装稳定规则

如果某个属性需要长期具备统一行为,比如类型检查、自动转换、权限控制、懒加载、缓存读取,那么描述符非常适合。

例如:

class PositiveIntegerField(IntegerField):
    def validate(self, value):
        value = super().validate(value)
        if value is not None and value < 0:
            raise ValidationError(f"{self.name} 不能为负数")
        return value

这样所有使用 PositiveIntegerField 的模型字段都会自动获得非负校验能力。

2. 不要滥用描述符制造“黑魔法”

描述符很强,但强大也意味着容易让代码变得难以理解。

如果一个属性访问背后隐藏了大量数据库查询、网络请求或复杂副作用,团队成员可能会很难排查性能问题。

例如:

for article in articles:
    print(article.author.name)

如果 article.author 是延迟加载字段,这段代码可能触发 N 次数据库查询,这就是经典的 N+1 查询问题。

解决方式通常是预加载:

articles = Article.objects.select_related("author")

所以,使用描述符时要注意:接口可以优雅,但行为必须可预期。

3. 查询表达式要避免直接拼接 SQL

前面示例中为了教学方便直接拼接了 SQL,但生产环境必须使用参数化查询。

错误示例:

sql = f"SELECT * FROM users WHERE name = '{name}'"

推荐方式:

sql = "SELECT * FROM users WHERE name = %s"
params = [name]

ORM 的查询表达式最终也应该生成 SQL 模板和参数列表,而不是直接生成完整字符串。

4. 描述符要考虑实例访问和类访问的差异

一个成熟的描述符通常都要处理这段逻辑:

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

因为:

user.name

和:

User.name

不是同一种访问场景。

前者通常用于读取字段值,后者通常用于获取字段元数据或构建查询表达式。

5. 优先保证可读性

ORM 的目标不是炫技,而是让业务代码更清晰。

好的 ORM 字段设计应该让人一眼能看懂模型结构:

class Product(Model):
    id = IntegerField(primary_key=True)
    name = StringField(max_length=100, nullable=False)
    price = IntegerField(nullable=False)

这比把字段规则散落在多个函数、配置文件和校验器中更直观。


十一、描述符在成熟 ORM 中的影子

在成熟 ORM 框架中,描述符常常以不同形式存在。

在 SQLAlchemy 中,模型类上的属性往往不是普通字段,而是带有 ORM 管理能力的属性对象。它们既可以在实例层面管理字段读写,也可以在类层面参与查询表达式构建。

在 Django ORM 中,模型字段声明之后,框架会在模型类上安装相应的属性访问机制,用于处理普通字段、延迟字段、外键关系、反向关系等场景。开发者写的是自然的属性访问,框架内部则承担了数据转换、关系查询和缓存管理。

这也是 ORM 的核心价值之一:它不是简单地把 SQL 换成 Python,而是把数据库交互抽象成符合面向对象习惯的编程模型。


十二、总结:描述符是 ORM 优雅接口背后的关键齿轮

描述符在 ORM 中通常承担以下职责:

  1. 字段映射:把模型属性映射到数据库字段;
  2. 属性访问控制:拦截读取、赋值和删除;
  3. 类型校验与数据清洗:保证写入数据符合规则;
  4. 默认值处理:提供字段默认行为;
  5. 脏字段追踪:记录哪些字段被修改;
  6. 延迟加载:在真正访问关联对象时再查询数据库;
  7. 关系管理:支持外键、一对多、多对多等关系访问;
  8. 查询表达式构建:让类属性参与 SQL 条件生成;
  9. 字段元数据管理:配合元类收集表结构信息。

如果说 ORM 是一座连接对象世界和关系数据库世界的桥梁,那么描述符就是桥上那些看不见却至关重要的铆钉。它让 user.name 这样的普通代码拥有了更丰富的语义,也让 Python 框架能够以优雅、自然、可扩展的方式组织复杂逻辑。

学习描述符,不只是为了理解一个语法点,更是为了看见 Python 语言设计中那种“简单表象之下隐藏强大协议”的美。

当你下一次写下:

user.email = "alice@example.com"

也许你会意识到,这一行代码背后,可能正有一个描述符在默默工作:它检查类型,记录变化,准备 SQL,维护缓存,并让整个系统保持干净、有序、可靠。

这就是 Python 的温柔之处。它把复杂留给框架,把清晰交给开发者。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铭渊老黄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值