Pydantic
https://docs.pydantic.dev/latest
Pydantic is the most widely used data validation library for Python.
Pydantic名称由来,python和pedantic的合成词,pedantic意为【过分拘泥于细节(或传统)的;学究气的;迂腐的】。
https://docs.pydantic.dev/latest/#why-use-pydantic
为什么使用Pydantic?验证和转换,https://docs.pydantic.dev/latest/why/
验证
(核心功能)Pydantic基于类型提示实现模式验证,可以自定义验证器。
Pydantic与IDE和静态分析工具结合,实现良好的代码提示,提高开发效率。
Pydantic核心验证逻辑由Rust开发,Pydantic是Python中最快的数据验库之一。
转换
严格模式和宽松模式——Pydantic 可以在 strict=True 模式(数据不进行转换)或 strict=False 模式下运行(在适当的情况下,Pydantic 尝试将数据强制转换为正确类型)
(核心功能)Pydantic schema可以直接生成JSON schema,可以自定义序列化器。
https://docs.pydantic.dev/latest/version-policy/
如何安装Pydantic?
pip install pydantic
Pydantic仅仅依赖:
- pydantic-core: Rust开发的Pydantic核心验证逻辑
- typing-extensions: 标准库 typing 的后端
- annotated-types: 可以与 typing.Annotated 共同使用的约束类型
Pycharm 插件
https://docs.pydantic.dev/latest/integrations/pycharm/
https://docs.pydantic.dev/latest/concepts/models/
类型验证
from typing import List
from pydantic import BaseModel, Field
class ModelAndFieldV2(BaseModel):
### 列表验证
# 列表长度验证
items: List[str] = Field(..., min_length=1, max_length=10)
# 列表元素唯一性验证
tags: List[str] = Field(...)
# 价格必须为正数的列表
prices: List[float] = Field(..., gt=0)
# None,type(None)或Literal[None]只允许None值
# bool 布尔类型
# int 整数类型
# float 浮点数类型
# str 字符串类型
# bytes 字节类型
# list 允许list,tuple,set,frozenset,deque, 或生成器并转换为列表
# tuple 允许list,tuple,set,frozenset,deque, 或生成器并转换为元组
# dict 字典类型
# set 允许list,tuple,set,frozenset,deque, 或生成器和转换为集合;
# frozenset 允许list,tuple,set,frozenset,deque, 或生成器和强制转换为冻结集
# deque 允许list,tuple,set,frozenset,deque, 或生成器和强制转换为双端队列
# datetime 的date,datetime,time,timedelta 等日期类型
# typing 中的 Deque, Dict, FrozenSet, List, Optional, Sequence, Set, Tuple, Union,Callable,Pattern等类型
# FilePath,文件路径
# DirectoryPath 目录路径
# EmailStr 电子邮件地址
# NameEmail 有效的电子邮件地址或格式
# PyObject 需要一个字符串并加载可在该虚线路径中导入的 python 对象;
# Color 颜色类型
# AnyUrl 任意网址
# SecretStr、SecretBytes 敏感信息,将被格式化为'**********'或''
# Json 类型
# PaymentCardNumber 支付卡类型
# 约束类型,可以使用con*类型函数限制许多常见类型的值
# conlist
# item_type: Type[T]: 列表项的类型
# min_items: int = None: 列表中的最小项目数
# max_items: int = None: 列表中的最大项目数
# conset
# item_type: Type[T]: 设置项目的类型
# min_items: int = None: 集合中的最小项目数
# max_items: int = None: 集合中的最大项目数
# conint
# strict: bool = False: 控制类型强制
# gt: int = None: 强制整数大于设定值
# ge: int = None: 强制整数大于或等于设定值
# lt: int = None: 强制整数小于设定值
# le: int = None: 强制整数小于或等于设定值
# multiple_of: int = None: 强制整数为设定值的倍数
# confloat
# strict: bool = False: 控制类型强制
# gt: float = None: 强制浮点数大于设定值
# ge: float = None: 强制 float 大于或等于设定值
# lt: float = None: 强制浮点数小于设定值
# le: float = None: 强制 float 小于或等于设定值
# multiple_of: float = None: 强制 float 为设定值的倍数
# condecimal
# gt: Decimal = None: 强制十进制大于设定值
# ge: Decimal = None: 强制十进制大于或等于设定值
# lt: Decimal = None: 强制十进制小于设定值
# le: Decimal = None: 强制十进制小于或等于设定值
# max_digits: int = None: 小数点内的最大位数。它不包括小数点前的零或尾随的十进制零
# decimal_places: int = None: 允许的最大小数位数。它不包括尾随十进制零
# multiple_of: Decimal = None: 强制十进制为设定值的倍数
# constr
# strip_whitespace: bool = False: 删除前尾空格
# to_lower: bool = False: 将所有字符转为小写
# strict: bool = False: 控制类型强制
# min_length: int = None: 字符串的最小长度
# max_length: int = None: 字符串的最大长度
# curtail_length: int = None: 当字符串长度超过设定值时,将字符串长度缩小到设定值
# regex: str = None: 正则表达式来验证字符串
# conbytes
# strip_whitespace: bool = False: 删除前尾空格
# to_lower: bool = False: 将所有字符转为小写
# min_length: int = None: 字节串的最小长度
# max_length: int = None: 字节串的最大长度
# 严格类型,您可以使用StrictStr,StrictBytes,StrictInt,StrictFloat,和StrictBool类型,以防止强制兼容类型
枚举验证
from enum import Enum
from pydantic import BaseModel, Field
class ModeEnum(int, Enum):
LOW = 1
MEDIUM = 2
HIGH = 3
OTHER = 3
class Model(BaseModel):
priority: ModeEnum = Field(...)
# 若枚举中有重复值,则生成的枚举是第一个匹配的值
m = Model(priority=3)
print(m)
print(type(m.model_dump(mode='python')))
print(Model(priority=4))
# 优点是pydantic会自动从从枚举中校验值
# 缺点是会把int转为枚举类型,使用的时候可能会造成不方便
# 此时就需要 print(type(m.model_dump(mode='json'))) 进行转换 而不是 python 模式
验证器装饰器
from pydantic import BaseModel, model_validator
class MyModel(BaseModel):
field_a: str = None
field_b: str = None
field_c: str = None
@model_validator(mode="after")
def only_one_field_set(self):
if (n := sum(map(lambda x: int(x is not None), [self.field_a, self.field_b, self.field_c]))) != 1:
raise ValueError(f'Only one of field_a, field_b, field_c can be set but got {n}')
return self
model = MyModel(field_a='value_a', field_b='value_b')
from typing import Any
from pydantic import BaseModel, ValidationError, model_validator, field_validator
from pydantic.v1.class_validators import ValidatorCallable
from pydantic_core.core_schema import ValidatorFunctionWrapHandler
class MyModel(BaseModel):
field_a: str = None
field_b: str = None
field_c: str = None
@model_validator(mode='wrap')
@classmethod
def model_validator_wrap(cls, data: Any, handler):
try:
print("model_validator_wrap")
return handler(data)
except ValidationError:
raise
@model_validator(mode="before")
@classmethod
def model_validator_before(cls, data: Any):
print("model_validator_before")
return data
@model_validator(mode="after")
def model_validator_after(self):
print("model_validator_after")
return self
@field_validator('field_a', mode='wrap')
@classmethod
def field_validator_wrap_a(cls, value: Any, handler: ValidatorFunctionWrapHandler) -> str:
try:
print("field_validator_wrap_a")
return handler(value)
except ValidationError:
raise
@field_validator('field_a', mode='before')
@classmethod
def field_validator_before_a(cls, value: Any) -> Any:
print("field_validator_before_a")
return value
@field_validator('field_a', mode='after')
@classmethod
def field_validator_after_a(cls, value: int) -> int:
print("field_validator_after_a")
return value
@field_validator('field_b', mode='wrap')
@classmethod
def field_validator_wrap_b(cls, value: Any, handler: ValidatorFunctionWrapHandler) -> str:
try:
print("field_validator_wrap_b")
return handler(value)
except ValidationError:
raise
@field_validator('field_b', mode='before')
@classmethod
def field_validator_before_b(cls, value: Any) -> Any:
print("field_validator_before_b")
return value
@field_validator('field_b', mode='plain')
@classmethod
def field_validator_plain_b(cls, value: Any) -> Any:
print("field_validator_plain_b")
return value
@field_validator('field_b', mode='after')
@classmethod
def field_validator_after_b(cls, value: int) -> int:
print("field_validator_after_b")
return value
model = MyModel(field_a='value_a', field_b='value_b', field_c='value_c')
# model_validator_wrap
# model_validator_before
# field_validator_before_a
# field_validator_wrap_a
# field_validator_after_a
# field_validator_plain_b
# field_validator_after_b
# model_validator_after
"""
https://docs.pydantic.dev/latest/concepts/validators/
https://docs.pydantic.org.cn/latest/api/fields/
1.10 中文文档 https://hellowac.github.io/pydantic-zh-cn/v1.10.7-zh-cn/
2版本中文文档 https://docs.pydantic.org.cn/latest/
如上:
- 对于字段:wrap > before > plain > after
- 对于模型:wrap > before > after
- 两者混合 wrap > before > (字段级别) > after
"""
常用自定义验证
import datetime
# 在 v2 中需要显式使用v1相关 API 以进行兼容
from pydantic.v1 import BaseModel, validator
from pydantic_core import ValidationError
class DateModel(BaseModel):
date: str
@validator('date', pre=True)
def parse_date(cls, value):
try:
# 尝试将字符串解析为日期,格式为 YYYY-MM-DD
return datetime.datetime.strptime(value, '%Y-%m-%d').date()
except ValueError:
# 如果解析失败,抛出错误
raise ValueError('Invalid date format. Expected YYYY-MM-DD.')
@validator('date')
def check_date(cls, value):
# 进一步检查日期是否有效
if not isinstance(value, datetime.date):
raise ValueError('date must be a valid date')
return value
# 使用模型进行验证
try:
date_model = DateModel(date='2023-01-01')
print(date_model)
except ValidationError as e:
print(e)
# 尝试一个无效的日期格式
try:
date_model = DateModel(date='01-01-2023')
except ValidationError as e:
print(e)
json dict 序列化相关
from datetime import datetime
from typing import Optional, List, Annotated
from pydantic import BaseModel, Field, field_validator, PositiveInt, constr, StringConstraints
class Model(BaseModel):
# 不要使用 constr 而是 Annotated 和 StringConstraints,因为 constr 是动态构造类型,不利于静态类型检查
c: constr(strip_whitespace=True, to_upper=True, pattern=r'^[A-Z]+$')
d: Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, pattern=r'^[A-Z]+$')]
name1: str | None = Field(None, min_length=1, max_length=50)
name2: str | None = Field(None, min_length=1, max_length=50)
operation_type: Optional[str] = Field(..., pattern='^(Bind|UnBind)$', title='操作类型')
account_ids: List[str] = Field(..., title='账户id列表')
signup_ts: datetime | None
date: str = Field(...)
tastes: dict[str, PositiveInt]
@field_validator("date")
@classmethod
def validate_date(cls, v):
try:
datetime.strptime(v, '%Y-%m-%d')
except Exception:
raise ValueError("时间字符串不合法")
@field_validator('account_ids')
@classmethod
def validate_accounts(cls, accounts):
if len(accounts) > 50:
raise ValueError('提交账户操作数量超过限制')
for account in accounts:
if len(account) > 50:
raise ValueError('账户长度过长')
return accounts
instance = Model(
c="CCC",
d='DDD',
name1=None,
operation_type='Bind',
account_ids=list(map(str, range(1, 51))),
date="2024-10-12",
signup_ts='2019-06-01 12:22',
tastes={
'wine': 9,
b'cheese': 7,
'cabbage': '1',
},
)
# 返回实际入参提供的字段
print(instance.model_fields_set)
# 转为字典
print(instance.model_dump(mode='python'))
print(instance.model_dump(mode='json'))
# exclude unset 适用于保存到数据库,这里包含默认值和主动设置的空值以及主动设置的非空值
print(instance.model_dump(exclude_unset=True))
print(instance.model_dump(exclude_defaults=True))
print(instance.model_dump(exclude_none=True))
# 转为JSON字符串
print(instance.model_dump_json())
# 将schema导出,以JSON字符串形式
print(instance.model_json_schema())
配置文件支持
# 如果您创建一个继承自BaseSettings的模型,模型初始化程序将尝试通过从环境中读取,来确定未作为关键字参数传递的任何字段的值
# 1. 先从环境变量读取,有就取改值,结束
# 2. 再从配置文件读取,有就取该值,结束
# 3. 最后还没有就取默认值,结束