强烈推荐一个 Python 神库——Pydantic
初识 Pydantic
当我第一次接触 FastAPI 时,不可避免地遇到了 Pydantic。在 FastAPI 的生态中,Pydantic 几乎是不可或缺的——它负责请求/响应数据的校验、序列化和转换。然而,我的初次体验并不顺利:它的学习曲线略显陡峭,而且似乎提供了多种方式来完成同一件事,却没有明确的“最佳实践”指引。
尽管如此,随着深入使用,我逐渐意识到 Pydantic 的强大之处。它不仅仅是一个数据校验工具,更是一个能极大提升代码健壮性和可维护性的库。如今,它已成为我最喜爱的 十大 Python 库 之一。
版本说明与注意事项
在深入探讨之前,有两点需要特别注意:
- 本文基于 Pydantic v2:Pydantic v1 和 v2 之间存在显著差异,许多旧版代码在新版本中已不适用。
- 谨慎使用 AI 辅助工具(如 ChatGPT/Gemini):它们给出的示例可能是 v1 和 v2 的混合体,容易导致兼容性问题。
Pydantic 是什么?
Pydantic 是一个基于 Python 类型注解的 数据验证与解析库,其核心功能包括:
- 数据校验(Validation):确保输入数据符合预期结构(如类型检查、必填字段、取值范围等)。
- 数据转换(Parsing & Conversion):自动将原始数据(如 JSON、字典)转换为 Python 对象,并支持自定义转换逻辑。
- 序列化(Serialization):将 Python 对象转换为 JSON、字典等格式,便于 API 传输或存储。
为什么选用 Pydantic?
- 与 Python 类型系统深度集成:利用 typing 模块(如 str, int, List, Optional)定义数据Model,减少样板代码。
- 运行时数据安全保障:自动校验数据,避免无效或恶意输入进入业务逻辑。
- 无缝集成 FastAPI、SQLAlchemy 等框架:在 Web API、数据库交互等场景下提供高效的数据处理能力。
在接下来的章节中,我们将通过实际示例深入探讨 Pydantic 的核心功能,帮助你掌握这一强大工具。
一个非常基本的例子
假设你有一个需要名字和姓氏的函数。你需要确保两者都存在并且它们是字符串。
from pydantic import BaseModel
class MyFirstModel(BaseModel):
first_name: str
last_name: str
validating = MyFirstModel(first_name="marc", last_name="nealer")
虽然这个例子有点傻,但它表明了几点。首先,你可以看到 Pydantic 类看起来几乎与 Python 数据类相同。其次要注意的是,与数据类不同,Pydantic 会检查值是否为字符串,如果不是,则会发出验证错误。
需要注意的是,像这里展示的 give 类型验证被称为默认验证。稍后我们将讨论在此之前和之后的验证。
更复杂一点
当涉及可选参数时,Pydantic 可以毫无问题地处理,但输入可能不符合你的预期
from pydantic import BaseModel
from typing import Union, Optional
classMySecondModel(BaseModel):
first_name: str
middle_name: Union[str, None] # 这意味着参数不必发送
title: Optional [str] # 这意味着参数应该发送,但可以为 None
last_name: str
因此,如果你使用 Union,并且选项为 None,那么无论参数是否存在,Pydantic 都可以接受。如果你使用 Optional[],它期望发送参数,即使参数为空。这种表示法可能符合你的预期,但我觉得有点奇怪。
由此,你可以看到我们可以使用类型库中的所有对象,并且 Pydantic 将对它们进行验证。
from pydantic import BaseModel
from typing import Union, List, Dict
from datetime import datetime
class MyThirdModel(BaseModel):
name: Dict[str: str]
skills: List[str]
holidays: List[Union[str, datetime]]
应用默认值
到目前为止,我们还没有讨论如果价值观缺失我们该怎么办。
from pydantic import BaseModel
class DefaultsModel(BaseModel):
first_name: str = "jane"
middle_names: list = []
last_name : str = "doe"
这看起来似乎很明显。然而,有一个问题,那就是列表的定义。如果你以这种方式编写Model,则只会创建一个列表对象,并且该对象在该Model的所有实例之间共享。字典等也是如此。
为了解决这个问题,我们需要引入 Field 对象。
from pydantic import BaseModel, Field
class DefaultsModel(BaseModel):
first_name: str = "jane"
middle_names: list = Field(default_factory=list)
last_name: str = "doe"
请注意,传递给默认工厂的是类或函数,而不是其实例。这会导致为Model的所有实例创建一个新实例。
如果你看过 Pydantic 的文档,就会发现 Field 类有很多不同的用法。然而,我使用 Pydantic 越多,就越少用到 Field 对象。它可以做很多事情,但也会让事情变得复杂。对于默认值和默认工厂来说,它是可行的。至于其他的,你会看到我在这里做了什么。
嵌套Model
我不太需要使用嵌套的 Pydantic Model,但我认为它很有用。嵌套非常简单
from pydantic import BaseModel
class NameModel(BaseModel):
first_name: str
last_name: str
class UserModel(BaseModel):
username: str
name: NameModel
自定义验证
虽然默认的类型验证已经很棒了,但我们总是需要超越它。Pydantic 提供了多种不同的方式,你可以添加自己的验证例程。
在开始研究这些之前,我们需要先讨论一下 Before 和 After 选项。正如我上面所说,绑定验证被视为默认验证,因此当 Pydantic 在字段上添加自定义验证时,它被定义为在此默认验证之前或之后。
对于我们稍后将讨论的Model验证,其含义有所不同。“之前”是指在对象初始化之前进行验证;“之后”是指在对象初始化完成后,其他验证也已完成。
字段验证
我们可以使用 Field() 对象定义验证,但随着我们对 Pydantic 的深入了解,过度使用 Field() 对象会让事情变得困难。我们也可以使用装饰器创建验证器,并声明它应该应用于哪些字段。我更喜欢使用带注释的验证器。它们简洁明了,易于理解。其他程序员可以轻松地理解你的操作。
from pydantic import BaseModel, BeforeValidator, ValidationError
import datetime
from typing import Annotated
def stamp2date(value):
if not isinstance(value, float):
raise ValidationError("incoming date must be a timestamp")
try:
res = datetime.datetime.fromtimestamp(value)
except ValueError:
raise ValidationError("Time stamp appears to be invalid")
return res
class DateModel(BaseModel):
dob: Annotated[datetime.datetime, BeforeValidator(stamp2date)]
本示例在默认验证之前验证数据。这非常有用,因为它让我们有机会更改和重新格式化数据以及进行验证。在本例中,我期望传递一个数值时间戳。我验证了这一点,然后将时间戳转换为 datetime 对象。默认验证期望的是 datetime 对象。
Pydantic 还提供了 AfterValidator 和 WrapValidator。前者在默认验证器之后运行,后者则像中间件一样,在验证器之前和之后执行操作。我们还可以应用多个验证器
from pydantic import BaseModel, BeforeValidator, AfterValidator, ValidationError
import datetime
from typing import Annotated
defone_year(value):
if value < datetime.datetime.today() - datetime.timedelta(days=365):
raise ValidationError("the date must be less than a year old")
return value
defstamp2date(value):
ifnot isinstance(value, float):
raise ValidationError("incoming date must be a timestamp")
try:
res = datetime.datetime.fromtimestamp(value)
except ValueError:
raise ValidationError("Time stamp appears to be invalid")
return res
classDateModel(BaseModel):
dob: Annotated[datetime.datetime, BeforeValidator(stamp2date), AfterValidator(one_year)]
大多数情况下,我使用 BeforeValidator。在很多情况下,转换传入的数据是必须的。当你想检查值的类型是否正确时,AfterValidator 非常有用。我还没用过 WrapValidator。我想听听用过的人的意见,因为我想了解它的用例。
在继续之前,我想举一个例子来说明多种类型需要可选的情况。或者更确切地说,参数是可选的。
from pydantic import BaseModel, BeforeValidator, ValidationError, Field
import datetime
from typing import Annotated
defstamp2date(value):
ifnot isinstance(value, float):
raise ValidationError("incoming date must be a timestamp")
try:
res = datetime.datetime.fromtimestamp(value)
except ValueError:
raise ValidationError("Time stamp appears to be invalid")
return res
classDateModel(BaseModel):
dob: Annotated[Annotated[datetime.datetime, BeforeValidator(stamp2date)] | None, Field(default=None)]
Model验证
来看一个简单的用例。我们有三个值,它们都是可选的,但至少必须发送其中一个。字段验证只会检查每个字段本身,所以在这里不太适用。这时,Model验证就派上用场了。
from pydantic import BaseModel, model_validator, ValidationError
from typing import Union, Any
classAllOptionalAfterModel(BaseModel):
param1: Union[str, None] = None
param2: Union[str, None] = None
param3: Union[str, None] = None
@model_validator(mode="after")
defthere_must_be_one(self):
ifnot (self.param1 or self.param2 or self.param3):
raise ValidationError("One parameter must be specified")
return self
classAllOptionalBeforeModel(BaseModel):
param1: Union[str, None] = None
param2: Union[str, None] = None
param3: Union[str, None] = None
@model_validator(mode="before")
@classmethod
defthere_must_be_one(cls, data: Any):
ifnot (data["param1"] or data["param2"] or data["param3"]):
raise ValidationError("One parameter must be specified")
return data
以上是两个示例。第一个是 After 验证。你会注意到它标记了 mode="after",并且将对象作为 self 传递。这是一个重要的区别。
Before 验证的流程截然不同。首先,你会看到带有 mode="before" 的 model_validation 装饰器。然后是 classmethod 装饰器。重要提示:你需要按此顺序同时指定 和 。
当我没有这样做时,我收到了一些非常奇怪的错误消息,因此这是需要注意的重要一点。
接下来你会注意到,类和传递给类的数据(参数)都作为参数传递给了该方法。验证是针对数据或传递的值进行的,这些值通常以字典的形式传递。验证结束时需要将数据对象传回,这表明你可以使用此方法来修改数据,就像 BeforeValidator 一样。
Alias
Alias非常重要,尤其是在处理传入数据并执行转换时。我们使用Alias来更改值的名称,或者在值未作为字段名传递时定位它们。
Pydantic 将 Alias 定义为验证 Alias(传入值的名称与字段不同)和序列化 Alias(验证后序列化或输出数据时更改名称)。
文档详细介绍了如何使用 Field() 对象定义 Alias,但这样做存在一些问题。同时定义默认值和 Alias 不起作用。不过,我们可以在 Model 级别而不是字段级别定义 Alias。
from pydantic import AliasGenerator, BaseModel, ConfigDict
class Tree(BaseModel):
model_config = ConfigDict(
alias_generator=AliasGenerator(
validation_alias=lambda field_name: field_name.upper(),
serialization_alias=lambda field_name: field_name.title(),
)
)
age: int
height: float
kind: str
t = Tree.model_validate({'AGE': 12, 'HEIGHT': 1.2, 'KIND': 'oak'})
print(t.model_dump(by_alias=True))
{'Age': 12, 'Height': 1.2, 'Kind': 'oak'}
我从文档中引用了这个例子,因为它有点简单,而且用处不大,但它确实展示了如何转换字段名称。这里需要注意的是,如果要使用序列化 Alias 序列化 Model,则需要设置by_alias=True。
下面我们一起学习一些使用 AliasChoices 和 AliasPath 对象的更有用的 Alias 示例。
AliasChoices
发送给你的数据中,同一个值会被赋予不同的字段名或列名,这种情况很常见。我敢打赌,如果你让十几个人给你发送一份包含姓和名的姓名列表,你肯定会得到不同的列名!
AliasChoices 定义与给定字段匹配的传入值名称列表。
from pydantic import BaseModel, ConfigDict, AliasGenerator, AliasChoices
aliases = {
"first_name": AliasChoices("fname", "surname", "forename", "first_name"),
"last_name": AliasChoices("lname", "family_name", "last_name")
}
class FirstNameChoices(BaseModel):
model_config = ConfigDict(
alias_generator=AliasGenerator(
validation_alias=lambda field_name: aliases.get(field_name, None)
)
)
title: str
first_name: str
last_name: str
此处代码定义一个字典,其中键是字段名称,值是 AliasChoices 对象。请注意,我在列表中包含了实际的字段名称。你可能使用它来转换和序列化要保存的数据,然后希望将其读回Model以供使用。因此,实际的字段名称应该包含在列表中。
AliasPath
大多数情况下,传入的数据并非扁平的,而是 JSON 格式的 blob,这些 blob 会被转换成字典,然后传递给Model。那么,如何将字段设置为字典或列表中的值呢?AliasPath 就是用来做这件事的。
from pydantic import BaseModel, ConfigDict, AliasGenerator, AliasPath
aliases = {
"first_name": AliasPath("name", "first_name"),
"last_name": AliasPath("name", "last_name")
}
class FirstNameChoices(BaseModel):
model_config = ConfigDict(
alias_generator=AliasGenerator(
validation_alias=lambda field_name: aliases.get(field_name, None)
)
)
title: str
first_name: str
last_name: str
obj = FirstNameChoices(**{"name":{"first_name": "marc", "last_name": "Nealer"},"title":"Master Of All"})
从上面的代码中可以看到,姓和名都存储在一个字典中。我使用了 AliasPath 来扁平化数据,将值从字典中提取出来,这样所有值都位于同一层级。
使用 AliasPath 和 AliasChoices
我们可以将这两者一起使用。
from pydantic import BaseModel, ConfigDict, AliasGenerator, AliasPath, AliasChoices
aliases = {
"first_name": AliasChoices("first_name", AliasPath("name", "first_name")),
"last_name": AliasChoices("last_name", AliasPath("name", "last_name"))
}
class FirstNameChoices(BaseModel):
model_config = ConfigDict(
alias_generator=AliasGenerator(
validation_alias=lambda field_name: aliases.get(field_name, None)
)
)
title: str
first_name: str
last_name: str
obj = FirstNameChoices(**{"name":{"first_name": "marc", "last_name": "Nealer"},"title":"Master Of All"})
写在最后
Pydantic 是一个超级优秀的库,但它也存在一些问题,就是实现同一件事的方法太多了。为了理解和使用我这里展示的示例,我付出了很多努力。我希望通过这些示例,你能比我更快地上手 Pydantic,并且减少很多工作量。
最后一件事。Pydantic 和 AI 服务。Chat-gtp、Gemini 等对 Pydantic 的问题给出的答案总是很古怪。就好像它无法确定自己使用的是 Pydantic V1 还是 V2,所以总是搞混。你甚至会听到“Pydantic 做不到”之类的话来反驳它能做到的事情。所以在使用库的时候最好避免使用这些库。