百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

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

off999 2025-05-25 14:50 3 浏览 0 评论

初识 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 做不到”之类的话来反驳它能做到的事情。所以在使用库的时候最好避免使用这些库。

相关推荐

用Python写一个深度优先搜索算法含注释说明

大家好!我是幻化意识流。今天我们用Python写一个深度优先搜索的代码,我做了注释说明,欢迎大家一起学习:#定义一个函数,用于深度优先搜索#参数:#graph:一个字典,表示图的邻接表#st...

用Python写一个图算法之最短路径算法含注释说明

大家好!我是幻化意识流。今天我们用Python写一个图算法之最短路径算法的代码,我做了注释说明,欢迎大家一起学习:以下是Dijkstra最短路径算法的Python实现,我们将使用邻接矩阵表示图。请...

物理老师教你学Python语言(下篇)

下篇:物理建模与综合项目核心目标:掌握微分方程数值解、面向对象编程和交互式可视化,构建可扩展的物理仿真系统第7章动态系统模拟7.1数值解法与经典力学案例1:弹簧振子动力学(欧拉法)importn...

python四个性能检测工具,包括函数的运行内存、时间等等...

这里总结了五个比较好的python性能检测工具,包括内存使用、运行时间、执行次数等方面。首先,来编写一个基础的python函数用于在后面的各种性能测试。defbase_func():for...

FastAPI:Python领域的高性能API开发利器

一、引言:为何选择FastAPI?在数字化时代,API(应用程序编程接口)如同数字世界的"神经网络",连接着各种软件系统。FastAPI作为Python生态中一颗冉冉升起的明星,凭借其...

5 个让代码更干净、更高效的 Python 好习惯

随着Python的日益流行,开发者采用良好的编码实践变得非常重要。无论你是初学者还是有经验的程序员,这五个习惯都将帮助你编写更干净、更高效、更易于维护的Python代码。1.在脚本中使用i...

神秘的 Ellipsis(...)/省略号:Python 中被忽视的合法语法

在许多代码片段中,三个点常被用来表示“此处省略”。但在Python中,输入...并不仅仅是个缩写,它是一个真正的表达式!简单语法:如何使用它?使用省略号非常简单,只需写三个点:就是这样!在P...

python类元编程示例-使用类型注解来检查转换属性值的类框架

参考《流程的python》第24章,用三种方式实现使用类型注解来检查转换属性值的类框架1__init_subclass__方式1.1代码实现fromcollections.abcimport...

python关键字35个简易说明(缺少2个没有注释)

序号关键字含义1False逻辑假2None空值3True逻辑真4and逻辑与5as作为6assert断言,用except捕捉exceptExceptionasy:7async8await9bre...

掌握5 个 Python关键程序,编写更清晰、更高效的代码

Python是一种强大且灵活的编程语言,但编写干净、可维护和高效的代码需要遵循最佳实践。无论你是初学者还是有经验的开发者,遵守良好的编程习惯都将节省时间、减少错误,并使你的代码更容易理解。以下是你...

开源人声分离音频标注工具—基于Python

前言之前一篇介绍过音频标注开源工具包,大家反馈不错,今天介绍一个更易用专用性的人声分离音频标注开源工具,工具地址在文末。工具简介此工具是基于wavesurfer.js与Flask开发。提供Web界面进...

用Python实现线性规划算法并做注释说明

大家好!我是幻化意识流。为了实现线性规划算法,我们可以使用Python中的pulp库。Pulp包含一系列的线性规划功能,包括许多常见算法的实现。下面是一个示例代码实现简单的线性规划问题:#import...

用python解决三角函数问题并作注释说明

大家好!我是幻化意识流。下面是使用Python解决三角函数问题的示例代码:importmath#引入math模块#定义角度变量(单位为弧度)angle=math.pi/...

少儿python编程:找出100以内能被3整除的数

常规编程方法:fornuminrange(101):ifnum>=3andnum%3==0:print(num,end=',')另外一种编程方法:fornum...

Python lambda表达式详解

Pythonlambda表达式详解1.基本概念lambda表达式是Python中创建匿名函数的快捷方式,适用于需要临时使用的小型函数。语法结构lambda参数列表:表达式与普通函数对比特性la...

取消回复欢迎 发表评论: