为了账号安全,请及时绑定邮箱和手机立即绑定

使用 Pydantic 的实用指南

标签:
Python

当我开始尝试使用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 服务。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消