从 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 类 | 数据库 |
|---|---|
User | users 表 |
id | id 字段 |
name | name 字段 |
age | age 字段 |
User(...) 实例 | 表中的一行记录 |
问题来了:name = StringField(...) 是类属性,但我们希望在实例上这样使用:
user = User(name="Alice", age=20)
print(user.name)
user.age = 21
ORM 需要在这些看似普通的属性操作中完成很多任务:
- 字段名绑定;
- 类型检查;
- 默认值处理;
- 数据校验;
- 数据库存取映射;
- 延迟加载;
- 脏字段追踪;
- 查询表达式构建。
而描述符正好可以接管属性访问和赋值,因此非常适合做 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)
当通过类访问时,instance 是 None。
这给 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 示例
下面我们把前面的能力组合起来,实现一个极简模型系统。
它支持:
- 字段声明;
- 字段校验;
- 脏字段追踪;
- SQL 插入语句生成;
- 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 中通常承担以下职责:
- 字段映射:把模型属性映射到数据库字段;
- 属性访问控制:拦截读取、赋值和删除;
- 类型校验与数据清洗:保证写入数据符合规则;
- 默认值处理:提供字段默认行为;
- 脏字段追踪:记录哪些字段被修改;
- 延迟加载:在真正访问关联对象时再查询数据库;
- 关系管理:支持外键、一对多、多对多等关系访问;
- 查询表达式构建:让类属性参与 SQL 条件生成;
- 字段元数据管理:配合元类收集表结构信息。
如果说 ORM 是一座连接对象世界和关系数据库世界的桥梁,那么描述符就是桥上那些看不见却至关重要的铆钉。它让 user.name 这样的普通代码拥有了更丰富的语义,也让 Python 框架能够以优雅、自然、可扩展的方式组织复杂逻辑。
学习描述符,不只是为了理解一个语法点,更是为了看见 Python 语言设计中那种“简单表象之下隐藏强大协议”的美。
当你下一次写下:
user.email = "alice@example.com"
也许你会意识到,这一行代码背后,可能正有一个描述符在默默工作:它检查类型,记录变化,准备 SQL,维护缓存,并让整个系统保持干净、有序、可靠。
这就是 Python 的温柔之处。它把复杂留给框架,把清晰交给开发者。
783

被折叠的 条评论
为什么被折叠?



