社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

墙裂推荐一个 Python 神库--Pydantic

数据STUDIO • 1 月前 • 81 次点击  


初识 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 模块(如 strintListOptional)定义数据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

class MySecondModel(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


def one_year(value):
    if value < datetime.datetime.today() - datetime.timedelta(days=365 ):
        raise ValidationError("the date must be less than a year old")
    return value


def stamp2date(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


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):
    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


class DateModel(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

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):
        ifnot (self.param1 or self.param2 or self.param3):
            raise ValidationError("One parameter must be specified")
        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):
        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 做不到”之类的话来反驳它能做到的事情。所以在使用库的时候最好避免使用这些库。


🏴‍☠️宝藏级🏴‍☠️ 原创公众号『数据STUDIO』内容超级硬核。公众号以Python为核心语言,垂直于数据科学领域,包括可戳👉 PythonMySQL数据分析数据可视化机器学习与数据挖掘爬虫  等,从入门到进阶!

长按👇关注- 数据STUDIO -设为星标,干货速递

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/182058
 
81 次点击