【Python中级技巧】Pydantic: 在Python里简化数据校验

Pydantic: 在Python里简化数据校验

by Harrison Hoffman Apr 10, 2024

原文链接:https://realpython.com/python-pydantic/#configuring-applications-with-basesettings

目录

  • Python的Pydantic库
    • 熟悉Pydantic
    • 安装Pydantic
    • 添加可选依赖
  • 使用模型
    • 使用Pydantic的BaseModels
    • 利用字段进行自定义和添加元数据
  • 使用校验器
    • 校验模型和字段
    • 使用校验装饰器来校验函数
  • 管理设置
    • 使用BaseSettings配置应用程序
    • 使用SettingsConfigDict来自定义设置
  • 总结

Pydantic 是一个强大的数据校验和设置管理库,专为 Python 设计,旨在增强代码库的鲁棒性和可靠性。从基本任务如检查变量是否为整数,到更复杂的任务,比如确保高度嵌套的字典键和值具有正确的数据类型,Pydantic 几乎可以处理任何数据校验场景,且不需要过多的样板代码。

在这个教程中,你将学习如何:

  • 使用 Pydantic 的 BaseModel 处理数据架构(data schema)
  • 为复杂的使用场景编写自定义校验器
  • 使用 Pydantic 的 @validate_call 校验函数参数
  • 使用 pydantic-settings 管理设置和配置应用程序

在整个教程中,你将通过实际例子了解 Pydantic 的功能,并在结束时为你自己的校验场景打下坚实的基础。在开始这个教程之前,如果你已具备中级的 Python 和面向对象编程知识将会更有帮助。


Python的Pydantic库

Python 的主要吸引力之一是它是一种动态类型语言。动态类型意味着变量类型是在运行时确定的,不同于在编译时就需要明确声明类型的静态类型语言。虽然动态类型对于快速开发和易用性来说是极好的,但在现实世界的应用中,你通常需要更鲁棒的类型检查和数据校验。这就是 Python 的 Pydantic 库发挥作用的地方。

Pydantic 迅速获得了流行,现在它是 Python 中使用最广泛的数据校验库。在这个第一部分,你将获得对 Pydantic 的概述和该库强大功能的预览。你还将学习如何安装 Pydantic 以及本教程所需的附加依赖项。


熟悉Pydantic

Pydantic 是一个功能强大的 Python 库,它利用类型提示来帮助你轻松校验和序列化你的数据架构。这使你的代码更加鲁棒、易读、简洁,并更易于调试。Pydantic 还与许多流行的静态类型工具和集成开发环境(IDE)很好地集成,这使你能够在运行代码之前捕获架构问题。

Pydantic 的一些显著特点包括:

  • **自定义:**Pydantic 能校验的数据类型几乎没有限制。从基本的 Python 类型到高度嵌套的数据结构,Pydantic 允许你校验和序列化几乎任何 Python 对象。

  • 灵活性: Pydantic 允许你在校验数据时按照希望来控制或严格、或宽松的策略。在某些情况下,你可能希望将传入的数据强制转换为正确的类型。例如,你可以接受本应为浮点数但实际上是整数的数据。在其他情况下,你可能想严格执行你接收的数据类型。Pydantic 使你能够做到这一点。

  • 序列化: 你可以将 Pydantic 对象序列化和反序列化为字典和 JSON 字符串。这意味着你可以无缝地将你的 Pydantic 对象转换为 JSON,反之亦然。这种能力导致了自记录(self-documenting)的 API 和与几乎任何支持 JSON 架构的工具的集成。

  • 性能: 由于其核心校验逻辑是用 Rust 编写的,Pydantic 非常快。这种性能优势为你提供了快速可靠的数据处理,特别是在需要扩展到大量请求的高吞吐量应用程序(如 REST API)中。

  • 生态系统和行业采用: Pydantic 是许多流行 Python 库(如 FastAPI、LangChain 和 Polars)的依赖项。它也被大多数大型科技公司以及许多其他行业使用。这证明了 Pydantic 的社区支持、可靠性和韧性。

这些是使 Pydantic 成为一个吸引人的数据校验库的一些关键特性,你将在本教程中看到这些特性的实际应用。接下来,你将对如何安装 Pydantic 及其各种依赖有一个总体了解。


安装Pydantic

Pydantic 可以在 PyPI 上找到,并且可以通过 pip 进行安装。打开一个终端或命令提示符,创建一个新的虚拟环境,然后运行以下命令来安装 Pydantic:

python -m pip install pydantic

这条命令将从 PyPI 安装最新版本的 Pydantic 到你的机器上。为了检验安装是否成功,启动一个 Python REPL 并导入 Pydantic:

import pydantic

如果导入操作没有发生错误,那么你就已经成功安装了 Pydantic,并且你的系统上现在已经安装了 Pydantic 的核心功能。


添加可选依赖

你也可以安装 Pydantic 的可选依赖项。例如,在本教程中,你将会用到电子邮件校验,你可以在安装时包含这些依赖项:

python -m pip install "pydantic[email]"

Pydantic 有一个用于设置管理的单独包,在这个教程中你也将学到这部分内容。要安装这个包,请运行以下命令:

python -m pip install pydantic-settings

通过以上步骤,你已经安装了本教程所需的所有依赖项,现在你已经准备好开始探索 Pydantic 了。你将从学习模型开始——这是 Pydantic 定义数据架构的主要方式。


使用模型

Pydantic 定义数据架构的主要方式是通过模型。Pydantic 模型是一个对象,类似于 Python 的数据类(dataclass),它通过带注解的字段定义并存储有关实体的数据。与数据类不同,Pydantic 的重点集中在自动数据解析、校验和序列化上。

理解这一点的最好方法是创建你自己的模型,这也是你接下来要做的事情。


使用Pydantic的BaseModels

假设你正在为人力资源部门构建一个用于管理员工信息的应用程序,并且你需要一种方式来校验新员工信息的正确形式。例如,每位员工应该拥有一个身份证号、姓名、电子邮件、出生日期、薪水、部门和福利选择。这是使用 Pydantic 模型的一个完美案例!

为了定义你的员工模型,你需要创建一个从 Pydantic 的 BaseModel 继承的类:

from datetime import date
from uuid import UUID, uuid4
from enum import Enum
from pydantic import BaseModel, EmailStr

class Department(Enum):
    HR = "HR"
    SALES = "SALES"
    IT = "IT"
    ENGINEERING = "ENGINEERING"

class Employee(BaseModel):
    employee_id: UUID = uuid4()
    name: str
    email: EmailStr
    date_of_birth: date
    salary: float
    department: Department
    elected_benefits: bool

首先,你需要导入定义员工模型所需的依赖。然后,你创建一个枚举来代表公司中的不同部门,并将其用于注释员工模型中的department字段。

接下来,你定义你的 Pydantic 模型,即Employee,它继承自 BaseModel 并通过注解定义员工字段的名称和预期类型。对于在 Employee 中定义的每个字段以及在实例化 Employee 对象时 Pydantic 如何校验它们,分步讲解如下:

  • **employee_id:**这是你想要存储信息的员工的 UUID。通过使用 UUID 注解,Pydantic 确保这个字段总是一个有效的 UUID。每个 Employee 实例默认都会被分配一个 UUID,正如你通过调用 uuid4() 指定的。
  • **name:**员工的名字,Pydantic 预期它是一个字符串。
  • **email:**Pydantic 将确保每个员工的email有效,通过在底层使用 Python 的 email-validator 库来实现。
  • **date_of_birth:**每个员工的出生日期必须是一个有效的日期,如 Python 的 datetime 模块中的 date 所注释的。如果你传入一个字符串到 date_of_birth,Pydantic 将尝试解析并将其转换为 date 对象。
  • **salary:**这是员工的薪水,预期是一个浮点数。
  • **department:**每个员工的部门必须是 HRSALESITENGINEERING 中的一个,如你的 Department 枚举所定义。
  • **elected_benefits:**该字段存储员工是否选择了福利,Pydantic 预期它是一个布尔值。

创建一个 Employee 对象的最简单方法是像实例化任何其他 Python 对象一样进行实例化。为此,打开一个 Python REPL 并运行以下代码:

>>> from pydantic_models import Employee

>>> Employee(
...     name="Chris DeTuma",
...     email="cdetuma@example.com",
...     date_of_birth="1998-04-02",
...     salary=123_000.00,
...     department="IT",
...     elected_benefits=True,
... )
Employee(
    employee_id=UUID('73636d47-373b-40cd-a005-4819a84d9ea7'),
    name='Chris DeTuma',
    email='cdetuma@example.com',
    date_of_birth=datetime.date(1998, 4, 2),
    salary=123000.0,
    department=<Department.IT: 'IT'>,
    elected_benefits=True
)

在这个代码块中,你导入了 Employee 并使用所有必需的员工字段创建了一个对象。Pydantic 成功地校验并强制转换了你传入的字段,并创建了一个有效的 Employee 对象。注意 Pydantic 自动将你的日期字符串转换为日期对象,以及将你的 IT 字符串转换为相应的 Department 枚举。

接下来,看看当你尝试向 Employee 实例传递无效数据时,Pydantic 如何响应:

>>> Employee(
...     employee_id="123",
...     name=False,
...     email="cdetumaexamplecom",
...     date_of_birth="1939804-02",
...     salary="high paying",
...     department="PRODUCT",
...     elected_benefits=300,
... )

Traceback (most recent call last):
pydantic_core._pydantic_core.ValidationError: 7 validation errors for
Employee

employee_id
  Input should be a valid UUID, invalid length: expected length 32 for
  simple format, found 3 [type=uuid_parsing, input_value='123',
  input_type=str] For further information visit
  https://errors.pydantic.dev/2.6/v/uuid_parsing

name
  Input should be a valid string [type=string_type, input_value=False,
  input_type=bool] For further information visit
  https://errors.pydantic.dev/2.6/v/string_type

email
  value is not a valid email address: The email address is not valid.
  It must have exactly one @-sign. [type=value_error,
  input_value='cdetumaexamplecom', input_type=str]

date_of_birth
  Input should be a valid date or datetime, invalid date separator,
  expected `-` [type=date_from_datetime_parsing,
  input_value='1939804-02', input_type=str] For further information
  visit https://errors.pydantic.dev/2.6/v/date_from_datetime_parsing

salary
  Input should be a valid number, unable to parse string as a number
  [type=float_parsing, input_value='high paying', input_type=str]
  For further information visit
  https://errors.pydantic.dev/2.6/v/float_parsing

department
  Input should be 'HR', 'SALES', 'IT' or 'ENGINEERING'
  [type=enum, input_value='PRODUCT', input_type=str]

elected_benefits
  Input should be a valid boolean, unable to interpret input
  [type=bool_parsing, input_value=300, input_type=int]
  For further information visit
  https://errors.pydantic.dev/2.6/v/bool_parsing

在这个例子中,你创建了一个包含无效数据字段的 Employee 对象。Pydantic 为每个字段提供了详细的错误信息,告诉你期望什么、收到了什么,以及你可以去哪里了解更多关于错误的信息。

这种详细的校验非常有力,因为它防止你在 Employee 中存储无效数据。当实例化 Employee 对象没有报错时,你就能确信它包含了你所期望的数据,并且你可以信任这些数据在你的代码中或其他应用程序中的下游处理。

Pydantic 的 BaseModel 配备了一套方法,使从其他对象(如字典和 JSON)创建模型变得容易。例如,如果你想从字典实例化一个 Employee 对象,你可以使用 .model_validate() 类方法:

>>> new_employee_dict = {
...     "name": "Chris DeTuma",
...     "email": "cdetuma@example.com",
...     "date_of_birth": "1998-04-02",
...     "salary": 123_000.00,
...     "department": "IT",
...     "elected_benefits": True,
... }

>>> Employee.model_validate(new_employee_dict)
Employee(
    employee_id=UUID('73636d47-373b-40cd-a005-4819a84d9ea7'),
    name='Chris DeTuma',
    email='cdetuma@example.com',
    date_of_birth=datetime.date(1998, 4, 2),
    salary=123000.0,
    department=<Department.IT: 'IT'>,
    elected_benefits=True
)

在这里,你创建了一个包含员工字段的字典 new_employee_dict,并将其传递给 .model_validate() 来创建一个 Employee 实例。在底层,Pydantic 校验每个字典条目以确保其符合你所期望的数据。如果有任何数据无效,Pydantic 将以你之前看到的相同方式抛出错误。如果字典中缺少任何字段,你也会得到通知。

你可以使用 .model_validate_json() 以同样的方式处理 JSON 对象:

>>> new_employee_json = """
...  {"employee_id":"d2e7b773-926b-49df-939a-5e98cbb9c9eb",
...  "name":"Eric Slogrenta",
...  "email":"eslogrenta@example.com",
...  "date_of_birth":"1990-01-02",
...  "salary":125000.0,
...  "department":"HR",
...  "elected_benefits":false}
...  """

>>> new_employee = Employee.model_validate_json(new_employee_json)
>>> new_employee
Employee(
    employee_id=UUID('d2e7b773-926b-49df-939a-5e98cbb9c9eb'),
    name='Eric Slogrenta',
    email='eslogrenta@example.com',
    date_of_birth=datetime.date(1990, 1, 2),
    salary=125000.0,
    department=<Department.HR: 'HR'>,
    elected_benefits=False
)

在这个例子中,new_employee_json 是一个有效的 JSON 字符串,存储了你的员工字段,你使用 .model_validate_json() 来校验并从 new_employee_json 创建一个 Employee 对象。虽然这看起来微不足道,但从 JSON 创建和校验 Pydantic 模型的能力非常强大,因为 JSON 是跨网络传输数据最流行的方式之一。这也是 FastAPI 依赖 Pydantic 创建 REST API 的原因之一。

你还可以将 Pydantic 模型序列化为字典和 JSON:

>>> new_employee.model_dump()
{
    'employee_id': UUID('d2e7b773-926b-49df-939a-5e98cbb9c9eb'),
    'name': 'Eric Slogrenta',
    'email': 'eslogrenta@example.com',
    'date_of_birth': datetime.date(1990, 1, 2),
    'salary': 125000.0,
    'department': <Department.HR: 'HR'>,
    'elected_benefits': False
}

>>> new_employee.model_dump_json()
'{"employee_id":"d2e7b773-926b-49df-939a-5e98cbb9c9eb","name":"Eric Slogrenta","email":"eslogrenta@example.com","date_of_birth":"1990-01-02","salary":125000.0,"department":"HR","elected_benefits":false}'

在这里,你使用 .model_dump().model_dump_json() 分别将你的 new_employee 模型转换为字典和 JSON 字符串。注意 .model_dump_json() 返回的 JSON 对象中,date_of_birthdepartment 被存储为字符串。

虽然 Pydantic 已经校验了这些字段并将你的模型转换为 JSON,但使用这个 JSON 的下游用户不会知道 date_of_birth 需要是一个有效的 datedepartment 需要是你的 Department 枚举中的一个类别。为了解决这个问题,你可以从你的 Employee 模型创建一个 JSON 架构(JSON schema)。

JSON 架构告诉你 JSON 对象中预期的字段是什么,以及这些字段代表的值是什么。你可以将其视为你的 Employee 类定义的 JSON 版本。以下是为 Employee 生成 JSON 架构的方法:

>>> Employee.model_json_schema()
{
    '$defs': {
        'Department': {
            'enum': ['HR', 'SALES', 'IT', 'ENGINEERING'],
            'title': 'Department',
            'type': 'string'
        }
    },
    'properties': {
        'employee_id': {
            'default': '73636d47-373b-40cd-a005-4819a84d9ea7',
            'format': 'uuid',
            'title': 'Employee Id',
            'type': 'string'
        },
        'name': {'title': 'Name', 'type': 'string'},
        'email': {
            'format': 'email',
            'title': 'Email',
            'type': 'string'
        },
        'date_of_birth': {
            'format': 'date',
            'title': 'Date Of Birth',
            'type': 'string'
        },
        'salary': {'title': 'Salary', 'type': 'number'},
        'department': {'$ref': '#/$defs/Department'},
        'elected_benefits': {'title': 'Elected Benefits', 'type': 'boolean'}
    },
    'required': [
        'name',
        'email',
        'date_of_birth',
        'salary',
        'department',
        'elected_benefits'
    ],
    'title': 'Employee',
    'type': 'object'
}

当你调用 .model_json_schema() 时,你会得到一个代表你模型的 JSON 架构的字典。你首先看到的条目显示了 department 可以采取的值。你还会看到关于你的字段应如何格式化的信息。例如,根据这个 JSON 架构,employee_id 预期为 UUID,而 date_of_birth 预期为日期。

你可以使用 json.dumps() 将你的 JSON 架构转换为 JSON 字符串,这使得几乎任何编程语言都能校验由你的 Employee 模型产生的 JSON 对象。换句话说,Pydantic 不仅可以校验传入的数据并将其序列化为 JSON,还可以为其他编程语言提供它们需要的信息,以通过 JSON 架构校验你的模型的数据。

至此,你现在已经了解如何使用 Pydantic 的 BaseModel 来校验和序列化你的数据。接下来,你将学习如何使用字段来进一步自定义你的校验。


利用字段进行自定义和添加元数据

到目前为止,你的 Employee 模型校验了每个字段的数据类型,并确保了一些字段,如 emaildate_of_birthdepartment,采取了有效的格式。但是,假设你还想确保 salary 是一个正数, name 不是空字符串,以及 email 包含你公司的域名。你可以使用 Pydantic 的 Field 类来实现这一点。

Field 类允许你自定义模型字段并添加元数据。要了解这是如何工作的,请看这个示例:

from datetime import date
from uuid import UUID, uuid4
from enum import Enum
from pydantic import BaseModel, EmailStr, Field

class Department(Enum):
    HR = "HR"
    SALES = "SALES"
    IT = "IT"
    ENGINEERING = "ENGINEERING"

class Employee(BaseModel):
    employee_id: UUID = Field(default_factory=uuid4, frozen=True)
    name: str = Field(min_length=1, frozen=True)
    email: EmailStr = Field(pattern=r".+@example\.com$")
    date_of_birth: date = Field(alias="birth_date", repr=False, frozen=True)
    salary: float = Field(alias="compensation", gt=0, repr=False)
    department: Department
    elected_benefits: bool

在这里,你导入了 Field 以及之前使用的其他依赖项,并为一些 Employee 字段设置了默认值。下面是你用来为字段添加额外校验和元数据的 Field 参数的细节解释:

  • default_factory:你使用这个参数定义一个可调用的生成默认值的函数。在上面的示例中,你将 default_factory 设置为 uuid4。这会在需要时调用 uuid4() 生成一个随机的 UUID 作为 employee_id。你也可以使用 lambda 函数来增加灵活性。
  • frozen:这是一个布尔参数,你可以设置它使你的字段不可变。这意味着,当 frozen 设置为 True 时,对应的字段在模型实例化后不能被更改。在这个示例中,employee_idnamedate_of_birth 使用 frozen 参数被设置为不可变。
  • min_length:你可以使用 min_lengthmax_length 控制字符串字段的长度。在上面的示例中,你确保 name 至少有一个字符长。
  • pattern:对于字符串字段,你可以将 pattern 设置为正则表达式,以匹配你期望的任何模式。例如,在上面的示例中,当你为 email 使用正则表达式时,Pydantic 将确保每个电子邮件都以 @example.com 结尾。
  • alias:当你想为你的字段分配一个别名时,可以使用这个参数。例如,你可以允许 date_of_birth 被称为 birth_date 或者 salary 被称为 compensation。你可以在实例化或序列化模型时使用这些别名。
  • gt:这个参数是 “greater than” 的缩写,用于数值字段设置最小值。在这个示例中,设置 gt=0 确保薪水总是正数。Pydantic 还有其他数值约束,比如 lt,即 “less than”。
  • repr:这个布尔参数决定是否在模型的字段表示中显示一个字段。在这个示例中,当你打印一个 Employee 实例时,你将不会看到 date_of_birthsalary

要看到这些额外校验的效果,请注意当你尝试用错误数据创建一个 Employee 模型时会发生什么:

>>> from pydantic_models import Employee

>>> incorrect_employee_data = {
...     "name": "",
...     "email": "cdetuma@fakedomain.com",
...     "birth_date": "1998-04-02",
...     "salary": -10,
...     "department": "IT",
...     "elected_benefits": True,
... }

>>> Employee.model_validate(incorrect_employee_data)
Traceback (most recent call last):

pydantic_core._pydantic_core.ValidationError: 3 validation errors for
Employee
name
  String should have at least 1 character [type=string_too_short,
   input_value='', input_type=str] For further information visit
    https://errors.pydantic.dev/2.6/v/string_too_short
email
  String should match pattern '.+@example\.com$'
  [type=string_pattern_mismatch,
  input_value='cdetuma@fakedomain.com', input_type=str]
    For further information visit
    https://errors.pydantic.dev/2.6/v/string_pattern_mismatch
salary
  Input should be greater than 0 [type=greater_than, input_value=-10,
  input_type=int] For further information visit
  https://errors.pydantic.dev/2.6/v/greater_than

在这里,你导入了更新后的 Employee 模型,并尝试校验一个包含错误数据的字典。作为响应,Pydantic 给出了三个校验错误: name 至少需要一个字符, email 应该匹配你公司的域名,salary 应该大于零。

现在请注意当你校验正确的 Employee 数据时获得的额外功能:

>>> employee_data = {
...     "name": "Clyde Harwell",
...     "email": "charwell@example.com",
...     "birth_date": "2000-06-12",
...     "compensation": 100_000,
...     "department": "ENGINEERING",
...     "elected_benefits": True,
... }

>>> employee = Employee.model_validate(employee_data)
>>> employee
Employee(
    employee_id=UUID('614c6f75-8528-4272-9cfc-365ddfafebd9'),
    name='Clyde Harwell',
    email='charwell@example.com',
    department=<Department.ENGINEERING: 'ENGINEERING'>,
    elected_benefits=True)

>>> employee.salary
100000.0

>>> employee.date_of_birth
datetime.date(2000, 6, 12)

在这个代码块中,你创建了一个字典和一个使用 .model_validate()Employee 模型。在 employee_data 中,注意到你使用了 birth_date 替代 date_of_birthcompensation 替代 salary。Pydantic 识别这些别名,并在内部将它们的值分配给正确的字段名。

因为你设置了 repr=False,你可以看到 salarydate_of_birth 并没有显示在 Employee 的表示中。你必须显式地作为属性访问它们才能看到它们的值。最后,请注意当你尝试改变一个冻结的字段时会发生什么:

>>> employee.department = "HR"
>>> employee.name = "Andrew TuGrendele"
Traceback (most recent call last):

pydantic_core._pydantic_core.ValidationError: 1
validation error for Employee
name
  Field is frozen [type=frozen_field, input_value='Andrew TuGrendele',
  input_type=str]
  For further information visit
  https://errors.pydantic.dev/2.6/v/frozen_field

在这里,你首先将 department 的值从 IT 改为 HR 。这是完全可以接受的,因为 department 不是一个冻结的字段。然而,当你尝试改变 name 时,Pydantic 给出了一个错误,说明 name 是一个冻结的字段。

现在你已经对 Pydantic 的 BaseModelField 类有了深入的理解。仅凭这些,你就可以在数据模型上定义许多不同的校验规则和元数据,但有时这还不够。接下来,你将通过 Pydantic 校验器,将你的字段校验推进到更深的层次。


使用校验器

到目前为止,你已经使用了 Pydantic 的 BaseModel 来校验具有预定义类型的模型字段,并通过 Field 进一步定制了你的校验。虽然仅凭 BaseModelField 你已经可以做到很多事,但对于需要自定义逻辑的更复杂的校验场景,你将需要使用 Pydantic 校验器。

通过校验器,你可以执行几乎任何你能在函数中表达的校验逻辑。接下来你将看到如何做到这一点。


校验模型和字段

继续以员工示例来说,假设你的公司有一个政策,即他们只雇佣至少十八岁的员工。每次你创建一个新的Employee对象时,你需要确保员工年龄超过十八岁。为此,你可以添加一个age字段,并使用Field类来强制员工至少达到十八岁。然而,这似乎有些多余,因为你已经存储了员工的出生日期。

一个更好的解决方案是使用Pydantic字段校验器。字段校验器允许你通过在模型中添加类方法来对BaseModel字段应用自定义校验逻辑。为了强制所有员工至少十八岁,你可以向Employee模型添加以下字段校验器:

from datetime import date
from uuid import UUID, uuid4
from enum import Enum
from pydantic import BaseModel, EmailStr, Field, field_validator

class Department(Enum):
    HR = "HR"
    SALES = "SALES"
    IT = "IT"
    ENGINEERING = "ENGINEERING"

class Employee(BaseModel):
    employee_id: UUID = Field(default_factory=uuid4, frozen=True)
    name: str = Field(min_length=1, frozen=True)
    email: EmailStr = Field(pattern=r".+@example\.com$")
    date_of_birth: date = Field(alias="birth_date", repr=False, frozen=True)
    salary: float = Field(alias="compensation", gt=0, repr=False)
    department: Department
    elected_benefits: bool

    @field_validator("date_of_birth")
    @classmethod
    def check_valid_age(cls, date_of_birth: date) -> date:
        today = date.today()
        eighteen_years_ago = date(today.year - 18, today.month, today.day)

        if date_of_birth > eighteen_years_ago:
            raise ValueError("Employees must be at least 18 years old.")

        return date_of_birth

在这个代码块中,你导入了field_validator并用它来装饰Employee中的一个类方法.check_valid_age()。字段校验器必须定义为类方法。在.check_valid_age()中,你计算了十八年前的今天的日期。如果员工的date_of_birth在那个日期之后,就会抛出一个错误。

要看看这个校验器是如何工作的,看看这个例子:

>>> from pydantic_models import Employee
>>> from datetime import date, timedelta

>>> young_employee_data = {
...     "name": "Jake Bar",
...     "email": "jbar@example.com",
...     "birth_date": date.today() - timedelta(days=365 * 17),
...     "compensation": 90_000,
...     "department": "SALES",
...     "elected_benefits": True,
... }

>>> Employee.model_validate(young_employee_data)
Traceback (most recent call last):

pydantic_core._pydantic_core.ValidationError:
1 validation error for Employee
birth_date
  Value error, Employees must be at least 18 years old.
  [type=value_error, input_value=datetime.date(2007, 4, 10),
  input_type=date]
  For further information visit
  https://errors.pydantic.dev/2.6/v/value_error

在这个例子中,你指定了一个birth_date,比当前日期晚了十七年。当你调用.model_validate()来校验young_employee_data时,你会得到一个错误,提示员工必须至少有十八岁。

和你想的一样,Pydantic的field_validator()允许你随意定制字段校验。然而,如果你想要比较多个字段之间的关系或者对整个模型进行校验,field_validator()是不适用的。这时,你需要使用模型校验器。

举个例子,假设你的公司只在IT部门招聘合同工。因此,IT工作者不符合享受福利的资格,他们的elected_benefits字段应该是False。你可以使用Pydantic的model_validator()来执行这个约束:

from typing import Self
from datetime import date
from uuid import UUID, uuid4
from enum import Enum
from pydantic import (
    BaseModel,
    EmailStr,
    Field,
    field_validator,
    model_validator,
)

class Department(Enum):
    HR = "HR"
    SALES = "SALES"
    IT = "IT"
    ENGINEERING = "ENGINEERING"

class Employee(BaseModel):
    employee_id: UUID = Field(default_factory=uuid4, frozen=True)
    name: str = Field(min_length=1, frozen=True)
    email: EmailStr = Field(pattern=r".+@example\.com$")
    date_of_birth: date = Field(alias="birth_date", repr=False, frozen=True)
    salary: float = Field(alias="compensation", gt=0, repr=False)
    department: Department
    elected_benefits: bool

    @field_validator("date_of_birth")
    @classmethod
    def check_valid_age(cls, date_of_birth: date) -> date:
        today = date.today()
        eighteen_years_ago = date(today.year - 18, today.month, today.day)

        if date_of_birth > eighteen_years_ago:
            raise ValueError("Employees must be at least 18 years old.")

        return date_of_birth

    @model_validator(mode="after")
    def check_it_benefits(self) -> Self:
        department = self.department
        elected_benefits = self.elected_benefits

        if department == Department.IT and elected_benefits:
            raise ValueError(
                "IT employees are contractors and don't qualify for benefits"
            )
        return self

此时,你将Python的Self类型和Pydantic的model_validator()加入到导入中。然后,创建一个方法.check_it_benefits(),如果员工属于IT部门且elected_benefits字段为True,该方法会引发错误。当你在@model_validator中将模式设置为after时,Pydantic会等到你实例化模型之后再运行.check_it_benefits()

注意:你可能已经注意到.check_it_benefits()使用了Python的Self类型进行注释。这是因为.check_it_benefits()返回Employee类的实例,而Self类型是对此进行注释的首选方式。如果你使用的是Python 3.11以下的版本,你将需要从typing_extensions中导入Self类型。

要查看你的新模型校验器的实际效果,请看下面这个例子:

>>> from pydantic_models import Employee

>>> new_employee = {
...     "name": "Alexis Tau",
...     "email": "ataue@example.com",
...     "birth_date": "2001-04-012",
...     "compensation": 100_000,
...     "department": "IT",
...     "elected_benefits": True,
... }

>>> Employee.model_validate(new_employee)
Traceback (most recent call last):

pydantic_core._pydantic_core.ValidationError: 1 validation error for
Employee
  Value error, IT employees are contractors and don't qualify for
  benefits.
  [type=value_error, input_value={'name': 'Alexis Tau',
  ...elected_benefits': True},
  input_type=dict]
    For further information visit
    https://errors.pydantic.dev/2.6/v/value_error

在这个例子中,你尝试创建一个属于 IT 部门且elected_benefits设置为TrueEmployee 模型。当你调用 .model_validate() 时,Pydantic 抛出一个错误,通知你 IT 部门的员工因为是合同工,不符合享受福利的条件。

通过模型校验器和字段校验器,你几乎可以实现任何你能想到的自定义校验。现在你应该已经有了坚实的基础,可以为自己的使用场景创建 Pydantic 模型了。接下来,你将转换思路,看看如何使用 Pydantic 来校验任意函数,而不仅仅是 BaseModel 字段。


使用校验装饰器来校验函数

尽管 BaseModel 是 Pydantic 用于校验数据架构的核心类,你还可以使用 Pydantic 来校验函数参数,通过使用 @validate_call 装饰器。这允许你创建具有详尽类型错误信息的鲁棒函数,而无需手动实现校验逻辑。

为了了解这是如何工作的,假设你正在编写一个函数,该函数在客户购买商品后向他们发送发票。你的函数需要接收客户的名字、电子邮件、购买的商品和总账单金额,并且构建并发送邮件。你需要校验所有这些输入,因为如果这些输入出错,可能会导致邮件未被发送、格式错误或客户被错误开票。

为此,你编写了以下函数:

import time
from typing import Annotated
from pydantic import PositiveFloat, Field, EmailStr, validate_call

@validate_call
def send_invoice(
    client_name: Annotated[str, Field(min_length=1)],
    client_email: EmailStr,
    items_purchased: list[str],
    amount_owed: PositiveFloat,
) -> str:

    email_str = f"""
    Dear {client_name}, \n
    Thank you for choosing xyz inc! You
    owe ${amount_owed:,.2f} for the following items: \n
    {items_purchased}
    """

    print(f"Sending email to {client_email}...")
    time.sleep(2)

    return email_str

首先,你需要导入编写和注解 send_invoice() 所需的依赖。然后,你创建了用 @validate_call 装饰的 send_invoice()。在执行 send_invoice() 之前,@validate_call 确保每个输入都符合你的注解。在这个案例中,@validate_call 会检查 client_name 是否至少有一个字符,client_email 是否格式正确,items_purchased 是否是字符串列表,以及 amount_owed 是否是正浮点数。

如果输入之一不符合你的注解,Pydantic 将抛出一个错误,类似于你已经看到的与 BaseModel 相关的错误。如果所有输入都有效,send_invoice() 将创建一个字符串并模拟将其发送给你的客户,过程中使用 time.sleep(2) 模拟发送延迟。

注意:你可能已经注意到 client_name 使用了 Python 的 Annotated 类型进行注解。通常,当你想提供有关函数参数的元数据时,可以使用 Annotated。当你需要校验一个具有由 Field 指定的元数据的函数参数时,Pydantic 推荐使用 Annotated

然而,如果你使用 default_factory 来分配一个默认值给你的函数参数,你应该直接将参数分配给一个 Field 实例。你可以在 Pydantic 的文档中看到此类用法的示例。

要看到 @validate_callsend_invoice() 的实际运行,打开一个新的 Python REPL 并运行以下代码:

>>> from validate_functions import send_invoice

>>> send_invoice(
...     client_name="",
...     client_email="ajolawsonfakedomain.com",
...     items_purchased=["pie", "cookie", 17],
...     amount_owed=0,
... )
Traceback (most recent call last):
pydantic_core._pydantic_core.ValidationError: 4 validation errors for
send_invoice
client_name
  String should have at least 1 character [type=string_too_short,
  input_value='', input_type=str]
    For further information visit
    https://errors.pydantic.dev/2.6/v/string_too_short
client_email
  value is not a valid email address: The email address is not valid.
  It must have exactly one @-sign. [type=value_error,
  input_value='ajolawsonfakedomain.com', input_type=str]
items_purchased.2
  Input should be a valid string [type=string_type, input_value=17,
  input_type=int]
    For further information visit
    https://errors.pydantic.dev/2.6/v/string_type
amount_owed
  Input should be greater than 0 [type=greater_than, input_value=0,
  input_type=int]
    For further information visit
    https://errors.pydantic.dev/2.6/v/greater_than

在这个例子中,你导入了 send_invoice() 并传入了无效的函数参数。Pydantic 的 @validate_call 识别到这一点,并抛出错误,告诉你 client_name 需要至少一个字符,client_email 是无效的,items_purchased 应该包含字符串,以及 amount_owed 应该大于零。

当你传入有效的输入时,send_invoice() 将按预期运行:

>>> email_str = send_invoice(
...     client_name="Andrew Jolawson",
...     client_email="ajolawson@fakedomain.com",
...     items_purchased=["pie", "cookie", "cake"],
...     amount_owed=20,
... )
Sending email to ajolawson@fakedomain.com...

>>> print(email_str)

    Dear Andrew Jolawson,

    Thank you for choosing xyz inc! You
    owe $20.00 for the following items:

    ['pie', 'cookie', 'cake']

虽然 @validate_call 不如 BaseModel 灵活,但你仍然可以使用它对函数参数进行强大的校验。这可以为你节省大量时间,并让你避免编写冗余的类型检查和校验逻辑。如果你之前做过这样的工作,你就会知道为每个函数参数编写断言语句有多么繁琐。对于许多用例,@validate_call 为你解决了这个问题。

在本教程的最后一部分,你将学习如何使用 Pydantic 进行设置管理和配置。


管理设置

配置 Python 应用程序最流行的方法之一是使用环境变量。环境变量是存在于操作系统中、位于 Python 代码之外的变量,但可以被你的代码或其他程序读取。你可能会希望将密钥、数据库凭证、API 凭证、服务器地址和访问令牌等数据存储为环境变量。

环境变量通常在开发和生产之间变化,并且许多包含敏感信息。因此,你需要一种鲁棒的方式来解析、校验并集成代码中的环境变量。这正是 pydantic-settings 的完美用例,你将在本节中探索这一点。


使用BaseSettings配置应用程序

pydantic-settings 是 Python 中管理环境变量最强大的方法之一,它已被像 FastAPI 这样的流行库广泛采用和推荐。你可以使用 pydantic-settings 创建类似于 BaseModel 的模型,这些模型能解析和校验环境变量。

pydantic-settings 中的主要类是 BaseSettings,它具有与 BaseModel 相同的所有功能。然而,如果你创建了一个从 BaseSettings 继承的模型,模型初始化器将尝试从环境变量中读取任何未作为关键字参数传递的字段。

为了了解这是如何工作的,假设你的应用程序需要连接到数据库和另一个 API 服务。你的数据库凭证和 API 密钥可能会随时间改变,通常取决于你部署的环境。为了处理这个问题,你可以创建以下 BaseSettings 模型:

from pydantic import HttpUrl, Field
from pydantic_settings import BaseSettings

class AppConfig(BaseSettings):
    database_host: HttpUrl
    database_user: str = Field(min_length=5)
    database_password: str = Field(min_length=10)
    api_key: str = Field(min_length=20)

在这段脚本中,你导入了创建 BaseSettings 模型所需的依赖项。注意你从 pydantic_settings 导入 BaseSettings 时使用了下划线而非破折号。然后你定义了一个继承自 BaseSettings 的模型,AppConfig,它存储关于你的数据库和 API 密钥的字段。在这个示例中,database_host 必须是一个有效的 HTTP URL,其余字段具有最小长度限制。

接下来,打开一个终端并添加以下环境变量。如果你使用的是 Linux、macOS 或 Windows Bash,你可以使用 export 命令来执行此操作:

(venv) $ export DATABASE_HOST="http://somedatabaseprovider.us-east-2.com"
(venv) $ export DATABASE_USER="username"
(venv) $ export DATABASE_PASSWORD="asdfjl348ghl@9fhsl4"
(venv) $ export API_KEY="ajfsdla48fsdal49fj94jf93-f9dsal"

你也可以在 Windows PowerShell 中设置环境变量。然后你可以打开一个新的 Python REPL 并实例化 AppConfig

>>> from settings_management import AppConfig

>>> AppConfig()
AppConfig(
    database_host=Url('http://somedatabaseprovider.us-east-2.com/'),
    database_user='username',
    database_password='asdfjl348ghl@9fhsl4',
    api_key='ajfsdla48fsdal49fj94jf93-f9dsal'
)

请注意,在实例化 AppConfig 时你没有指定任何字段名称。相反,你的 BaseSettings 模型从你设置的环境变量中读取字段。同时请注意,你以全大写字母导出环境变量,但 AppConfig 依然成功解析并存储了它们。这是因为 BaseSettings 在匹配环境变量与字段名称时不区分大小写。

接下来,关闭你的 Python REPL 并创建无效的环境变量:

(venv) $ export DATABASE_HOST="somedatabaseprovider.us-east-2"
(venv) $ export DATABASE_USER="usee"
(venv) $ export DATABASE_PASSWORD="asdf"
(venv) $ export API_KEY="ajf"

现在打开另一个Python REPL 然后再次实例化 AppConfig

>>> from settings_management import AppConfig

>>> AppConfig()
Traceback (most recent call last):
pydantic_core._pydantic_core.ValidationError: 4 validation errors for
AppConfig
database_host
  Input should be a valid URL, relative URL without a base
  [type=url_parsing, input_value='somedatabaseprovider.us-east-2',
  input_type=str]
    For further information visit
    https://errors.pydantic.dev/2.6/v/url_parsing
database_user
  String should have at least 5 characters [type=string_too_short,
  input_value='usee', input_type=str]
    For further information visit
    https://errors.pydantic.dev/2.6/v/string_too_short
database_password
  String should have at least 10 characters [type=string_too_short,
  input_value='asdf', input_type=str]
    For further information visit
    https://errors.pydantic.dev/2.6/v/string_too_short
api_key
  String should have at least 20 characters [type=string_too_short,
  input_value='ajf', input_type=str]
    For further information visit
    https://errors.pydantic.dev/2.6/v/string_too_short

这次,当你尝试实例化 AppConfig 时,pydantic-settings 会抛出错误,指出 database_host 不是一个有效的 URL,并且其余字段没有达到最小长度要求。

虽然这是一个简化的配置示例,你可以利用 BaseSettings 来解析和校验几乎任何你需要从环境变量中获取的内容。你能在 BaseModel 中做的任何校验,也可以用 BaseSettings 来完成,包括使用模型和字段校验器进行自定义校验。

最后,你将学习如何通过 SettingsConfigDict 进一步自定义 BaseSettings 的行为。


使用SettingsConfigDict来自定义设置

在之前的示例中,你看到了一个创建 BaseSettings 模型的基本例子,该模型用于解析和校验环境变量。然而,你可能想进一步自定义你的 BaseSettings 模型的行为,你可以通过 SettingsConfigDict 来实现这一点。

假设你不能手动导出每个环境变量,这通常是常见的情况,你需要从 .env 文件中读取它们。你希望确保 BaseSettings 在解析时是区分大小写的,并且你的 .env 文件中没有除你在模型中指定的环境变量之外的其他环境变量。下面是你如何通过 SettingsConfigDict 来做到这一点的示例:

from pydantic import HttpUrl, Field
from pydantic_settings import BaseSettings, SettingsConfigDict

class AppConfig(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        case_sensitive=True,
        extra="forbid",
    )

    database_host: HttpUrl
    database_user: str = Field(min_length=5)
    database_password: str = Field(min_length=10)
    api_key: str = Field(min_length=20)

这个脚本与之前的示例相同,不同之处在于这次你导入了 SettingsConfigDict 并在 AppConfig 中初始化它。在你的 SettingsConfigDict 中,你指定环境变量应从 .env 文件中读取,应强制执行大小写敏感性,并且 .env 文件中禁止存在额外的环境变量。

接下来,在与 settings_management.py 同一目录下创建一个名为 .env 的文件,并用以下环境变量填充它:

database_host=http://somedatabaseprovider.us-east-2.com/
database_user=username
database_password=asdfjfffffl348ghl@9fhsl4
api_key=ajfsdla48fsdal49fj94jf93-f9dsal

现在打开一个Python REPL 然后实例化 AppConfig

>>> from settings_management import AppConfig

>>> AppConfig()
AppConfig(
    database_host=Url('http://somedatabaseprovider.us-east-2.com/'),
    database_user='username',
    database_password='asdfjfffffl348ghl@9fhsl4',
    api_key='ajfsdla48fsdal49fj94jf93-f9dsal'
)

如你所见,AppConfig 成功地解析并校验了你的 .env 文件中的环境变量。

最后,在你的 .env 文件中添加一些无效的变量:

DATABASE_HOST=http://somedatabaseprovider.us-east-2.com/
database_user=username
database_password=asdfjfffffl348ghl@9fhsl4
api_key=ajfsdla48fsdal49fj94jf93-f9dsal
extra_var=shouldntbehere

在这里,你将 database_host 更改为 DATABASE_HOST,违反了大小写敏感的限制,并且添加了不应存在的额外环境变量。下面是你的模型在尝试校验这些变量时的响应:

>>> from settings_management import AppConfig

>>> AppConfig()
Traceback (most recent call last):
pydantic_core._pydantic_core.ValidationError: 3 validation errors for
AppConfig
database_host
  Field required [type=missing, input_value={'database_user':
  'userna..._var': 'shouldntbehere'}, input_type=dict]
    For further information visit
    https://errors.pydantic.dev/2.6/v/missing
DATABASE_HOST
  Extra inputs are not permitted [type=extra_forbidden,
  input_value='http://somedatabaseprovider.us-east-2.com/', input_type=str]
    For further information visit
    https://errors.pydantic.dev/2.6/v/extra_forbidden
extra_var
  Extra inputs are not permitted [type=extra_forbidden,
  input_value='shouldntbehere', input_type=str]
    For further information visit
    https://errors.pydantic.dev/2.6/v/extra_forbidden

你将看到一系列错误信息,说明 database_host 缺失以及你的 .env 文件中有多余的环境变量。请注意,由于大小写敏感的限制,你的模型认为 DATABASE_HOST 以及 extra_var 是多余的变量。

使用 SettingsConfigDictBaseSettings 你可以做更多的事情,但这些例子应该已经让你明白如何使用 pydantic-settings 来管理你自己的环境变量。


总结

Pydantic 是一个易用、快速且广受信赖的 Python 数据校验库。你已经对 Pydantic 有了广泛的了解,现在你拥有了使用 Pydantic 在你自己的项目中的知识和资源。

在本教程中,你学到了

  • Pydantic 是什么,为什么它如此广泛被采用
  • 如何安装 Pydantic
  • 如何使用 BaseModel校验器解析、校验和序列化数据架构
  • 如何使用 @validate_call 编写函数的自定义校验逻辑
  • 如何使用 pydantic-settings 解析和校验环境变量

Pydantic 使你的代码更加鲁棒和可信,部分弥合了 Python 的易用性与静态类型语言内置的数据校验之间的差距。对于你可能有的几乎任何数据解析、校验和序列化的用例,Pydantic 都提供了优雅的解决方案。


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值