强烈推荐一个 Python 神库——Pydantic

初识 Pydantic

当我第一次接触 FastAPI 时,不可避免地遇到了 Pydantic。在 FastAPI 的生态中,Pydantic 几乎是不可或缺的——它负责请求/响应数据的校验、序列化和转换。然而,我的初次体验并不顺利:它的学习曲线略显陡峭,而且似乎提供了多种方式来完成同一件事,却没有明确的“最佳实践”指引。

尽管如此,随着深入使用,我逐渐意识到 Pydantic 的强大之处。它不仅仅是一个数据校验工具,更是一个能极大提升代码健壮性和可维护性的库。如今,它已成为我最喜爱的 十大 Python 库 之一。

版本说明与注意事项

在深入探讨之前,有两点需要特别注意:

  1. 本文基于 Pydantic v2:Pydantic v1 和 v2 之间存在显著差异,许多旧版代码在新版本中已不适用。
  2. 谨慎使用 AI 辅助工具(如 ChatGPT/Gemini):它们给出的示例可能是 v1 和 v2 的混合体,容易导致兼容性问题。

Pydantic 是什么?

Pydantic 是一个基于 Python 类型注解的 数据验证与解析库,其核心功能包括:

  • 数据校验(Validation):确保输入数据符合预期结构(如类型检查、必填字段、取值范围等)。
  • 数据转换(Parsing & Conversion):自动将原始数据(如 JSON、字典)转换为 Python 对象,并支持自定义转换逻辑。
  • 序列化(Serialization):将 Python 对象转换为 JSON、字典等格式,便于 API 传输或存储。

为什么选用 Pydantic?

  1. 与 Python 类型系统深度集成:利用 typing 模块(如 str, int, List, Optional)定义数据Model,减少样板代码。
  2. 运行时数据安全保障:自动校验数据,避免无效或恶意输入进入业务逻辑。
  3. 无缝集成 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 还提供了 AfterValidatorWrapValidator。前者在默认验证器之后运行,后者则像中间件一样,在验证器之前和之后执行操作。我们还可以应用多个验证器

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

下面我们一起学习一些使用 AliasChoicesAliasPath 对象的更有用的 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 做不到”之类的话来反驳它能做到的事情。所以在使用库的时候最好避免使用这些库。

相关文章

SQL用了两年多,分享2个最常用的小技巧

导读:SQL是所有数据从业者必须打牢的基本功之一,扎实的SQL查询和适当的调优技巧是检验SQL能力的两大重要准则。个人曾经专门花费过好多时间用于提升SQL能力,期间也刷了大量的SQL题目,在这期间也...

不允许你还不会的Python 文件与字符串处理高效技巧

掌握文件和字符串的高效处理技巧是Python编程中的重要能力。以下是一些专业级的优化技巧和实践方法:一、文件处理高效技巧1. 文件读取优化1.1 大文件逐行读取# 标准方法(内存友好) with op...

深入详解Python的比较运算

在Python编程中,比较运算是构建逻辑判断的核心基础,无论是条件控制、数据筛选还是算法逻辑,都离不开对数据的比较操作。本文将从基础概念到高级应用,全面解析Python比较运算的细节,帮助开发者深入理...

2025-05-01:第一个几乎相等子字符串的下标。用go语言,给定两个

2025-05-01:第一个几乎相等子字符串的下标。用go语言,给定两个字符串 s 和 pattern。如果字符串 x 修改 最多一个字符 之后能够变成字符串 y,则称 x 与 y 几乎相等。请你在函...

Python中 is 和 == 的区别

在 Python 中,is 和 == 都是用于比较两个对象的运算符,但它们的区别在于比较的内容不同。== 运算符用于比较两个对象的 值 是否相等。对于数字、字符串等不可变对象,== 比较的是它们的值是...