当我开始尝试使用FastAPI时,遇到了Pydantic。使用FastAPI时,你别无选择。然而,我对这个库的最初印象并不好。它有一个相对陡峭的学习曲线,并且似乎有很多方法可以做同样的事情,却没有一个明确的说法,“哦,除非……否则请使用这种方法”。
说起来,一旦理解了 Pydantic,它真是一个非常强大的工具。它是我最喜欢的前十位 Python 库之一。
在继续之前,需要指出的是,本文档讨论的是 Pydantic v2.*。版本 1 和版本 2 之间存在重大差异。我还建议您不要使用 ChatGTP 或 Gemini 来帮助编写 Pydantic 代码。它们给出的结果是版本 1 和版本 2 的奇怪混合。
Pydantic 是什么Pydantic 是带有验证、序列化和数据转换功能的 Python 数据类。因此,你可以使用 Pydantic 来检查你的数据是否有效,将数据转换成你需要的格式,然后序列化结果以便可以在其他应用程序之间传输。
一个非常基础的例子假设你有一个函数,该函数期望接收一个名字和姓氏。你需要确保这两个参数都存在并且都是字符串。
from pydantic import BaseModel
class MyFirstModel(BaseModel):
first_name: str
last_name: str
validating = MyFirstModel(first_name="marc", last_name="nealer")
虽然这个例子有点傻,但它展示了几个要点。首先,你可以看到 Pydantic 类几乎与 Python 的 dataclasses 一样。第二个需要注意的地方是,与 dataclass 不同,Pydantic 会检查值是否为字符串,并在不是字符串时发出验证错误。
需要注意的是,根据给定的类型进行验证,如这里所示,被称为默认验证。稍后我们将讨论在此点之前的验证和之后的验证。
让我们来稍微复杂一点当涉及到可选参数时,Pydantic 可以轻松处理,但类型可能不会是你期望的那样
from pydantic import BaseModel
from typing import Union, Optional
class MySecondModel(BaseModel):
first_name: str
middle_name: Union[str, None] # 这意味着该参数不必发送
title: Optional[str] # 这意味着该参数应该发送,但可以是 None
last_name: str
所以如果你使用 Union
,并将 None
作为选项之一,那么 Pydantic 会接受参数是否存在。如果你使用 Optional[]
,它会期望参数被发送,即使它是空的。这种表示方法可能是你期望的,但我感觉有点奇怪。
从这里可以看出,我们可以使用 typing
库中的所有对象,并且 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 默认值模型(BaseModel):
名字: str = "jane"
中间名: list = []
姓: str = "doe"
这看起来挺明显的。不过有一个问题,那就是列表的定义。如果你用这种方式编写模型,只会创建一个列表对象,并且这个对象会在该模型的所有实例之间共享。同样的情况也会发生在字典等数据结构上。
为了解决这个问题,我们需要引入 Field 对象。
从 pydantic 导入 BaseModel, Field
类 DefaultsModel(BaseModel):
first_name: str = "jane"
middle_names: list = Field(default_factory=list)
last_name: str = "doe"
注意,传递给默认工厂的是类或函数,而不是该类或函数的实例。这会导致为模型的所有实例创建一个新的实例。
如果你查阅过 Pydantic 的文档,你会发现 Field 类被以各种不同的方式使用。然而,我使用 Pydantic 越多,就越少使用 Field 对象。它可以做很多事情,但也会让事情变得复杂。对于默认值和默认工厂,使用它是个不错的选择。对于其他情况,你在这里会看到我的做法。
嵌套模型我不经常使用嵌套的Pydantic模型,但可以理解它会很有用。嵌套非常简单
从 pydantic 导入 BaseModel
类 NameModel(BaseModel):
first_name: str
last_name: str
类 UserModel(BaseModel):
username: str
name: NameModel
自定义验证
虽然通过类型进行默认验证非常棒,但我们总是需要做得更多。Pydantic 提供了多种不同的方式,让你可以添加自己的验证程序。
在我们开始查看这些内容之前,我们需要讨论一下 Before 和 After 选项。如上所述,绑定验证被视为默认选项,因此当 Pydantic 在字段上添加自定义验证时,它会被定义为在默认选项之前或之后。
带有模型验证,我们稍后会稍作讨论,含义就不同了。Before指的是在对象初始化之前进行验证,而after则是在对象已经初始化并且其他验证完成后进行。
字段验证我们可以使用 Field()
对象定义验证,但随着我们更深入地了解 Pydantic,过度使用 Field()
对象会使工作变得复杂。我们还可以使用装饰器来创建验证器,并指定其应用的字段。我更喜欢使用 Annotated
验证器。它们整洁且易于理解,其他程序员也能够轻松地理解你的代码。
from pydantic import BaseModel, BeforeValidator, ValidationError
import datetime
from typing import Annotated
def stamp2date(value):
if not isinstance(value, float):
raise ValidationError("传入的日期必须是时间戳")
try:
res = datetime.datetime.fromtimestamp(value)
except ValueError:
raise ValidationError("时间戳似乎无效")
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
def one_year(value):
if value < datetime.datetime.today() - datetime.timedelta(days=365):
raise ValidationError("日期必须不超过一年")
return value
def stamp2date(value):
if not isinstance(value, float):
raise ValidationError("传入的日期必须是时间戳")
try:
res = datetime.datetime.fromtimestamp(value)
except ValueError:
raise ValidationError("时间戳无效")
return res
class DateModel(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
def stamp2date(value):
if not isinstance(value, float):
raise ValidationError("传入的日期必须是时间戳")
try:
res = datetime.datetime.fromtimestamp(value)
except ValueError:
raise ValidationError("时间戳似乎无效")
return res
class DateModel(BaseModel):
dob: Annotated[Annotated[datetime.datetime, BeforeValidator(stamp2date)] | None, Field(default=None)]
模型验证
让我们来看一个简单的用例。我们有三个值,它们都是可选的,但至少需要发送一个。字段验证只检查每个字段本身,所以在这里不起作用。这时就需要模型验证了。
from pydantic import BaseModel, model_validator, ValidationError
from typing import Union, Any
class AllOptionalAfterModel(BaseModel):
param1: Union[str, None] = None
param2: Union[str, None] = None
param3: Union[str, None] = None
@model_validator(mode="after")
def there_must_be_one(self):
if not (self.param1 or self.param2 or self.param3):
raise ValidationError("必须指定一个参数")
return self
class AllOptionalBeforeModel(BaseModel):
param1: Union[str, None] = None
param2: Union[str, None] = None
param3: Union[str, None] = None
@model_validator(mode="before")
@classmethod
def there_must_be_one(cls, data: Any):
if not (data["param1"] or data["param2"] or data["param3"]):
raise ValidationError("必须指定一个参数")
return data
以上是两个示例。第一个是后验证。你会注意到它标记为 mode=”after”,并且传入的对象是 self。这是一个重要的区别。
前置验证遵循一条完全不同的路径。首先,你可以看到带有 mode="before"
参数的 model_validation
装饰器,然后是 classmethod
装饰器。重要的是,你需要按此顺序同时指定这两个装饰器。
我没有这样做时收到了一些非常奇怪的错误消息,所以这一点需要注意。
接下来你会注意到,类和传递给类的数据(参数)都会作为参数传递给方法。对数据或传递的值进行验证,这些值通常以字典的形式传递。在验证结束时需要将数据对象返回,因此你可以使用此方法来修改数据,就像使用 BeforeValidator 一样。
别名的别名很重要,尤其是在处理传入数据并执行转换时。我们使用别名来更改值的名称,或者在值未作为字段名传递时定位这些值。
Pydantic 定义了别名,包括验证别名(传入的值名称与字段名称不同)和序列化别名(在验证后序列化或输出数据时更改名称)。
文档详细介绍了如何使用 Field()
对象定义别名,但这样做存在一些问题。同时定义默认值和别名不起作用。不过,我们可以在模型级别而不是字段级别定义别名。
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'}
我从文档中取了这个例子,虽然它比较简单,实际用途不大,但它确实展示了如何转换字段名。需要注意的是,如果你想使用序列化别名来序列化模型,你需要设置“by_alias=True”。
现在让我们来看一些更实用的例子,展示如何使用 AliasChoices 和 AliasPath 对象来创建别名。
别名选择发送给你的数据中,给定的值使用了不同的字段或列名,这种情况非常常见。让你问十个人发送一个包含名字和姓氏的名单,每个人用的列名可能都不一样!!
AliasChoices 允许你定义一个传入值名称的列表,这些名称将与给定字段匹配。
从 pydantic 导入 BaseModel, ConfigDict, AliasGenerator, AliasChoices
别名 = {
"first_name": AliasChoices("fname", "surname", "forename", "first_name"),
"last_name": AliasChoices("lname", "family_name", "last_name")
}
类 FirstNameChoices(BaseModel):
模型配置 = ConfigDict(
别名生成器=AliasGenerator(
验证别名=lambda 字段名: 别名.get(字段名, None)
)
)
标题: str
first_name: str
last_name: str
此处展示的代码允许你定义一个字典,其中键是字段名称,值是 AliasChoices 对象。请注意,我已经在列表中包含了实际的字段名称。你可能使用这个来转换并序列化数据以进行保存,然后希望将其读回模型中使用。因此,实际的字段名称应该包含在列表中。
别名路径在大多数情况下,传入的数据不是扁平化的,而是以json块的形式出现,这些json块会被转换成字典,然后传递给你的模型。那么,我们如何将字段设置为字典或列表中的值呢?这就是 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
代号 = {
"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: 代号.get(field_name, None)
)
)
title: str
first_name: str
last_name: str
对象 = FirstNameChoices(**{"name":{"first_name": "marc", "last_name": "Nealer"},"title":"Master Of All"})
最终想法
Pydantic 是一个超级出色的库,但确实存在很多做同一件事的方法。为了理解并使用我在这里展示的例子,我花费了很多努力。希望你们能通过这些示例更快地掌握 Pydantic,并且付出的努力比我少得多。
最后一件事。关于 Pydantic 和 AI 服务。Chat-gtp、Gemini 等在回答关于 Pydantic 的问题时会给出不一致的答案。感觉它无法确定是 Pydantic V1 还是 V2,会把它们混在一起。甚至会出现“Pydantic 不能做那件事”这种明明可以做到的回答。所以在使用该库时,最好避免使用这些 AI 服务。
共同学习,写下你的评论
评论加载中...
作者其他优质文章