在处理来自系统外部的数据,如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)$")
功能
- 类型检查:
product_id
和quantity
必须是整数。 - 范围验证:
product_id
必须大于0,quantity
必须在1到100之间。 - 正则匹配:
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