Pydantic库-数据验证和设置管理

在处理来自系统外部的数据,如API、终端用户输入或其他来源时,我们必须牢记开发中的一条基本原则:“永远不要相信用户的输入”。

因此,我们必须对这些数据进行严格的检查和验证,确保它们被适当地格式化和标准化。这样做的目的是为了确保这些数据符合我们的程序所需的输入规范,从而保障项目能够正确且高效地运行。

为什么使用 Python 的 Pydantic 库?
Pydantic 是一个在 Python 中用于数据验证和解析的第三方库,它现在是 Python 使用最广泛的数据验证库。

它利用声明式的方式定义数据模型和Python 类型提示的强大功能来执行数据验证和序列化,使您的代码更可靠、更可读、更简洁且更易于调试。。
它还可以从模型生成 JSON 架构,提供了自动生成文档等功能,从而轻松与其他工具集成。


Pydantic 的一些主要特性
易用性

Pydantic 使用起来简单直观,需要最少的样板代码和配置。它适用于许多流行的 IDE 和静态分析工具,例如 PyCharm、VS Code、mypy 等。Pydantic 可以轻松与其他流行的 Python 库(如 Flask、Django、FastAPI 和 SQLAlchemy)集成,使其易于在现有项目中使用。

类型注解

Pydantic 使用类型注解来定义模型的字段类型,以确保确保数据符合预期的类型和格式。你可以使用Python 内置的类型、自定义类型或者其他Pydantic 提供的验证类型。

数据验证,用户友好的错误

Pydantic 自动根据模型定义进行数据验证。它会检查字段的类型、长度、范围等,并自动报告验证错误,Pydantic 会提供信息丰富且可读的错误消息,包括错误的位置、类型和输入。你可以使用 ValidationError 异常来捕获验证错误。

序列化与反序列化

序列化是将复杂数据结构(如对象、数组、字典等)转换为简单数据格式(如字符串或字节流)的过程。这样做是为了便于存储或传输。反序列化是相反的过程,将简单数据格式还原为复杂数据结构。Pydantic 提供了从各种数据格式(例如 JSON、字典)到模型实例的转换功能。它可以自动将输入数据解析成模型实例,并保留类型安全性和验证规则。

性能高

Pydantic 的核心验证逻辑是用 Rust 编写的,使其成为 Python 中最快的数据验证库之一。它还支持延迟验证和缓存,以提高效率。


要使用可以直接安装

pip install pydantic

Pydantic 使用例子

from datetime import datetime

from pydantic import BaseModel, PositiveInt


class User(BaseModel):
    id: int  #id 的类型是 int ;仅注释声明告知 Pydantic 该字段是必需的。如果可能,字符串、字节或浮点数将被强制转换为整数;否则将引发异常。
    name: str = 'John Doe'  #name 是一个字符串;因为它有默认值,所以不必需。
    signup_ts: datetime | None  #signup_ts 是一个必填的 datetime 字段,但值 None 可以提供;Pydantic 将处理 Unix 时间戳整数(例如 1496498400 )或表示日期和时间的字符串。
    tastes: dict[str, PositiveInt]  #tastes 是一个键为字符串且值为正整数的字典。 PositiveInt 类型是 Annotated[int, annotated_types.Gt(0)] 的简写。


external_data = {
    'id': 123,
    'signup_ts': '2019-06-01 12:22',  #这里的输入是一个 ISO8601 格式的日期时间,Pydantic 将把它转换为一个 datetime 对象。
    'tastes': {
        'wine': 9,
        b'cheese': 7,  #关键在这里是 bytes ,但 Pydantic 会负责将其强制转换为字符串。
        'cabbage': '1',  #同样地,Pydantic 会将字符串 '1' 强制转换为整数 1
    },
}

user = User(**external_data)  #这里通过将外部数据作为关键字参数传递给 User 来创建 User 的实例

print(user.id)  #我们可以将字段作为模型的属性来访问
#> 123
print(user.model_dump())  #我们可以将模型转换为带有 model_dump() 的字典
"""
{
    'id': 123,
    'name': 'John Doe',
    'signup_ts': datetime.datetime(2019, 6, 1, 12, 22),
    'tastes': {'wine': 9, 'cheese': 7, 'cabbage': 1},
}
"""

如果验证失败,Pydantic 会引发一个错误并详细说明哪里出错了:

# continuing the above example...

from pydantic import ValidationError


class User(BaseModel):
    id: int
    name: str = 'John Doe'
    signup_ts: datetime | None
    tastes: dict[str, PositiveInt]


external_data = {'id': 'not an int', 'tastes': {}}  

try:
    User(**external_data)  
except ValidationError as e:
    print(e.errors())
    """
    [
        {
            'type': 'int_parsing',
            'loc': ('id',),
            'msg': 'Input should be a valid integer, unable to parse string as an integer',
            'input': 'not an int',
            'url': 'https://pydantic.com.cn/errors/validation_errors#int_parsing',
        },
        {
            'type': 'missing',
            'loc': ('signup_ts',),
            'msg': 'Field required',
            'input': {'id': 'not an int', 'tastes': {}},
            'url': 'https://pydantic.com.cn/errors/validation_errors#missing',
        },
    ]
    """

简单解释

Pydantic 允许你定义数据模型,这些模型会自动验证输入数据的结构和类型。你只需定义一个类,用 Python 的类型提示标注其字段,Pydantic 就会为你处理验证和序列化。与使用JSON Schema或OpenAPI进行手动验证相比,这大大简化了数据验证过程。同时,Pydantic 也提供了强大的数据转换能力,能将复杂数据结构(如 JSON、字典)轻易转换为Python对象。

场景:API 参数验证和转换在电子商务平台

背景

假设你正在开发一个电子商务平台的后端服务,该服务提供了一个API端点,允许客户提交订单。每个订单都有多个字段,如产品ID、数量、支付方式等。你希望验证这些输入参数的有效性并转换为内部使用的Python对象。

常见技术对比

  • 手动验证:你可以在代码中手动检查每个字段,但这样做很冗长,容易出错。
  • JSON Schema:提供一种结构化的验证方法,但需要额外的定义和解析步骤。
  • Marshmallow:也是一种常用于数据验证的库,但与Pydantic相比,它更侧重于序列化和反序列化,而不是类型安全。

Pydantic 的实际应用

使用 Pydantic,你可以定义一个 Order模型来自动完成这些工作。

from pydantic import BaseModel, Field

class Order(BaseModel):
    product_id: int = Field(..., gt=0)
    quantity: int = Field(..., gt=0, le=100)
    payment_method: str = Field(..., regex="^(credit_card|paypal)$")

功能

  1. 类型检查product_id和 quantity必须是整数。
  2. 范围验证product_id必须大于0,quantity必须在1到100之间。
  3. 正则匹配payment_method只能是 “credit_card” 或 “paypal”。

使用

当客户通过API提交一个订单时,你只需将输入数据传递给这个 Order模型。如果数据无效,Pydantic 将自动抛出一个详细的错误,指出哪个字段无效以及为什么。

order_data = {
    "product_id": 1,
    "quantity": 50,
    "payment_method": "credit_card"
}

try:
    order = Order(**order_data)
except ValidationError as e:
    print(e.json())

这种方式使得代码更简洁,更易于维护,同时提供了强类型和自动验证的优点。与手动验证或使用其他库相比,Pydantic 提供了一个更为高效和直观的解决方案。

示例:用户注册API与Pydantic的数据验证

代码设置

在这个示例中,我们使用 FastAPI 构建一个简单的用户注册 API。FastAPI 与 Pydantic 集成非常紧密,用于请求和响应模型的验证。我们将比较使用 Pydantic 和手动验证的差异。

首先,我们导入必要的模块并设置 FastAPI 应用。

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr, Field
from typing import Optional

app = FastAPI()

Pydantic 数据模型

接下来,我们使用 Pydantic 定义一个用户注册的数据模型。

class UserRegister(BaseModel):
    username: str = Field(..., min_length=3, max_length=50)
    email: EmailStr
    password: str = Field(..., min_length=8)
    age: Optional[int] = Field(None, ge=18)

在这个模型中,我们定义了如下字段和验证规则:

  • username: 字符串类型,长度必须在3到50字符之间。
  • email: 必须是有效的电子邮件地址。
  • password: 字符串类型,至少包含8个字符。
  • age: 整数类型,可选,但如果提供必须大于等于18。

FastAPI 路由与验证

使用 Pydantic 模型,我们可以很容易地在 FastAPI 路由中进行数据验证。

@app.post("/register/")
def register(user: UserRegister):
    return {"username": user.username, "email": user.email}

对比:手动验证

如果不使用 Pydantic,数据验证会变得复杂和冗长。例如:

@app.post("/register_manual/")
def register_manual(username: str, email: str, password: str, age: Optional[int] = None):
    if len(username) < 3 or len(username) > 50:
        raise HTTPException(status_code=400, detail="Invalid username length")

    # ...其他字段验证

    return {"username": username, "email": email}

在这个手动验证的示例中,我们需要为每个字段写多行验证代码,这显然不如使用 Pydantic 效率高。

pydantic的核心是模型(Model)

验证数据

一旦你定义了模型,你可以使用它来验证数据。

如果要从字典实例化 User 对象,可以使用字典对象解包.model_validate().model_validate_json()类方法:

if __name__ == '__main__':

    user_data = {
        "id": 123,
        "name": "小卤蛋",
        "age": 20,
        "email": "xiaoludan@example.com",
        'signup_ts': '2024-07-19 00:22',
        'friends': ["公众号:海哥python", '小天才', b''],
        'password': '123456',
        'phone': '13800000000',
        'sex': '男'
    }

    try:
        # user = User(**user_data)
        user = User.model_validate(user_data)
        print(f"User id: {user.id}, User name: {user.name}, User email: {user.email}")
    except ValidationError as e:
        print(f"Validation error: {e.json()}")

都符合模型定义的情况下,您可以像往常一样访问模型的属性:

User id: 123, User name: 小卤蛋, User email: xiaoludan@example.com
1
如果数据不符合模型的定义(以下故意不传 id 字段),Pydantic 将抛出一个 ValidationError。

自定义验证

除了内置的验证器,还可以为模型定义自定义验证器。假设要确保用户年龄在18岁以上,可以使用@field_validator装饰器创建一个自定义验证器:

# ! -*-conding: UTF-8 -*-
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, EmailStr, field_validator, ValidationError


def check_name(v: str) -> str:
    """Validator to be used throughout"""
    if not v.startswith("小"):
        raise ValueError("must be startswith 小")
    return v


class User(BaseModel):
    id: int
    name: str = "小卤蛋"
    age: int
    email: EmailStr
    signup_ts: Optional[datetime] = None
    friends: List[str] = []

    validate_fields = field_validator("name")(check_name)
'''上面这行代码是使用field_validator装饰器来为name字段添加一个自定义的验证函数check_name。field_validator装饰器允许你为模型的字段指定一个或多个验证函数,这些函数将在模型实例化时自动调用,以确保字段值符合特定的条件。
"name":指定了要验证的字段名。
check_name:是一个自定义的验证函数,它接受一个字符串参数v,并检查这个字符串是否以"小"开头。如果不是,它将抛出一个ValueError。

下面的@field_validator("age")是一个装饰器,用于为age字段添加一个自定义的验证函数。@field_validator装饰器的工作方式与field_validator类似,但它是作为一个装饰器直接应用于方法上的,而不是作为类属性。
"age":指定了要验证的字段名。
check_age:是一个类方法,它接受一个参数age,并检查这个值是否小于18。如果是,它将抛出一个ValueError。
'''

    @field_validator("age")
    @classmethod
    def check_age(cls, age):
        if age < 18:
            raise ValueError("用户年龄必须大于18岁")
        return age

当尝试创建一个只有12岁的小朋友用户:

if __name__ == '__main__':
    user_data = {
        "id": 123,
        "name": "小卤蛋",
        "age": 12,
        "email": "xiaoludan@example.com",
        'signup_ts': '2024-07-19 00:22',
        'friends': ["公众号:海哥python", '小天才', b''],
    }
    try:
        user = User(**user_data)
    except ValidationError as e:
        print(f"Validation error: {e.json()}")

将得到一个错误:
Validation error: [{"type":"value_error","loc":["age"],"msg":"Value error, 用户年龄必须大于18岁","input":12,"ctx":{"error":"用户年龄必须大于18岁"},"url":"https://errors.pydantic.dev/2.8/v/value_error"}]

或者,当name不是小开头的话也会报错

如果要同时动态校验多个字段,还可以使用model_validator装饰器。
# ! -*-conding: UTF-8 -*-
# @公众号: 海哥python
from datetime import datetime
from typing import List, Optional
from typing_extensions import Self  # 如果python版本不低于3.11,则可以直接从typing中导入Self
from pydantic import BaseModel, ValidationError, EmailStr, field_validator, model_validator


def check_name(v: str) -> str:
    """Validator to be used throughout"""
    if not v.startswith("小"):
        raise ValueError("must be startswith 小")
    return v


class User(BaseModel):
    id: int
    name: str = "小卤蛋"
    age: int
    email: EmailStr
    signup_ts: Optional[datetime] = None
    friends: List[str] = []

    validate_fields = field_validator("name")(check_name)

    @field_validator("age")
    @classmethod
    def check_age(cls, age):
        if age < 18:
            raise ValueError("用户年龄必须大于18岁")
        return age

    @model_validator(mode="after")
    def check_age_and_name(self) -> Self:
        if self.age < 30 and self.name != "小卤蛋":
            raise ValueError("用户年龄必须小于30岁, 且名字必须为小卤蛋")

        return self


if __name__ == '__main__':
    user_data = {
        "id": 123,
        "name": "小小卤蛋",
        "age": 20,
        "email": "xiaoludan@example.com",
        'signup_ts': '2024-07-19 00:22',
        'friends': ["公众号:海哥python", '小天才', b''],
    }
    try:
        user = User(**user_data)
        print(user.model_dump())
    except ValidationError as e:
        print(f"Validation error: {e.json()}")

深入学习以下

https://blog.csdn.net/python_9k/article/details/140711001

https://blog.csdn.net/weixin_43936332/article/details/131627430

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注