之前在学习 FastAPI的时候,FastAPI 构建 API 高性能的 web 框架(一) 看到pydantic 使用场景非常多,也单独来看看这个模块。

pydantic的官方文档:https://docs.pydantic.dev/latest/
Usage界面:https://docs.pydantic.dev/latest/usage/models/

pydantic 在运行时强制执行类型提示,并在数据无效时提供友好的错误。

它具有如下优点:

  • 与 IDE/linter 完美搭配,不需要学习新的模式,只是使用类型注解定义类的实例
  • 多用途,BaseSettings 既可以验证请求数据,也可以从环境变量中读取系统设置
  • 快速
  • 可以验证复杂结构
  • 可扩展,可以使用validator装饰器装饰的模型上的方法来扩展验证
  • 数据类集成,除了BaseModel,pydantic还提供了一个dataclass装饰器,它创建带有输入数据解析和验证的普通 Python 数据类。

同时可以检查的python格式包括:

  • 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 有效的电子邮件地址或格式


1 BaseModel 基本用法

1.1 基本属性

BaseModel的基本属性包括:

  • dict() 模型字段和值的字典
  • json() JSON 字符串表示dict()
  • copy() 模型的副本(默认为浅表副本)
  • parse_obj() 使用dict解析数据
  • parse_raw 将str或bytes并将其解析为json,然后将结果传递给parse_obj
  • parse_file 文件路径,读取文件并将内容传递给parse_raw。如果content_type省略,则从文件的扩展名推断
  • from_orm() 从ORM 对象创建模型
  • schema() 返回模式的字典
  • schema_json() 返回该字典的 JSON 字符串表示
  • construct() 允许在没有验证的情况下创建模型
  • fields_set 初始化模型实例时设置的字段名称集
  • fields 模型字段的字典
  • config 模型的配置类

1.2 基本属性验证用法代码案例

先来个比较简单的版本:

from pydantic import BaseModel

class User(BaseModel):
    id: int
     name: str = 'John Doe'  # name是字符型,同时设定了一个默认值

定义了一个User模型,继承自BaseModel,有2个字段,id是一个整数并且是必需的,name是一个带有默认值的字符串并且不是必需的

实例化使用:

# 情况一:因为定义了User类中id是数字,所以这里实例化后,如果可以变成数字的,直接转化
user = User(id='123')
>>> '{"id": 123, "name": "Jane Doe"}'

# 情况二:定义id为整数,且不可以转化为整数,则会如上报错
user = User(id='123a')
>>> ValidationError: 1 validation error for User
id
  value is not a valid integer (type=type_error.integer)

# 情况三:定义id为整数,此时是float格式,也会报错
user = User(id='123.2')
>>> ValidationError: 1 validation error for User
id
  value is not a valid integer (type=type_error.integer)

# 情况四:限制文字只在以下几个出现
from typing import Literal, Union
class Lizard(BaseModel):
    pet_type: Literal['reptile', 'lizard']
    scales: bool



再来一段复杂点的案例:

from pydantic import ValidationError

# 这里规定了id必须是int类型
class User(BaseModel):
    id: int
    name: str = 'John Doe'
    signup_ts: datetime | None # 字符型或者None都是可以的
    tastes: dict[str, PositiveInt] # 字典型,规定key是字符型,value一定是正整数型
    address: Optional[Address] # Optional可选是否填写
    bool_value: bool 
    # 正负取值,print(BooleanModel(bool_value='False'))

# 然而这里id给了个str类型
external_data = {'id': 'not an int', 'tastes': {}}  

try:
    User(**external_data)  
except ValidationError as e:
    print(e.errors())

关于List / Tuple这类的限定:
参考:文档

from typing import Deque, List, Optional, Tuple

from pydantic import BaseModel


class Model(BaseModel):
    simple_list: Optional[list] = None
    list_of_ints: Optional[List[int]] = None

    simple_tuple: Optional[tuple] = None
    tuple_of_different_types: Optional[Tuple[int, float, bool]] = None

    deque: Optional[Deque[int]] = None


print(Model(simple_list=['1', '2', '3']).simple_list)
#> ['1', '2', '3']
print(Model(list_of_ints=['1', '2', '3']).list_of_ints)
#> [1, 2, 3]

print(Model(simple_tuple=[1, 2, 3, 4]).simple_tuple)
#> (1, 2, 3, 4)
print(Model(tuple_of_different_types=[3, 2, 1]).tuple_of_different_types)
#> (3, 2.0, True)

print(Model(deque=[1, 2, 3]).deque)
#> deque([1, 2, 3])

可以嵌套比较复杂的结构,同时都是可选的,同时嵌套结构可以进行定义

1.3 约束参数范围

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 typing import Annotated, Dict, List, Literal, Tuple
from annotated_types import Gt
from pydantic import BaseModel


class Fruit(BaseModel):
    name: str  
    color: Literal['red', 'green']   
    # 规定color是字符型,同时只能在red / green两个文本中进行选择
    
    weight: Annotated[float, Gt(0)]  
    
    bazam: Dict[str, List[Tuple[int, bool, float]]]   
    # 定义bazam为字符型,key为str型,value为List型,这里的嵌套结构比较多


print(
    Fruit(
        name='Apple',
        color='red',
        weight=4.2,
        bazam={'foobar': [(1, True, 0.1)]},
    )
)


1.4 Fields的用法

https://docs.pydantic.dev/latest/usage/fields/

from pydantic import BaseModel, Field
from uuid import uuid4
# 设置默认值
class User(BaseModel):
    name: str = Field(default='John Doe')
    id: int = Field(default_factory=lambda: uuid4().hex) # 每个姓名需要hash化

user = User()
print(user)
#> name='John Doe'

# 设置数字范围
from pydantic import BaseModel, Field

class Foo(BaseModel):
    positive: int = Field(gt=0) # 大于
    non_negative: int = Field(ge=0) # 大于等于
    negative: int = Field(lt=0) # 小于
    non_positive: int = Field(le=0) # 小于等于
    even: int = Field(multiple_of=2) # 是2的倍数
    love_for_pydantic: float = Field(allow_inf_nan=True) # 允许nan / inf值
    short: str = Field(min_length=3) # 允许字符最短3
    long: str = Field(max_length=10) # 允许字符最长10
    regex: str = Field(pattern=r'^\d*$')  # 字符需要进行正则化
    precise: Decimal = Field(max_digits=5, decimal_places=2) # max_digits 不论小数点前后总位数5个  / decimal_places 小数点后面的最大2位

foo = Foo(
    positive=1,
    non_negative=0,
    negative=-1,
    non_positive=0,
    even=2,
    love_for_pydantic=float('inf'),
)
print(foo)
"""
positive=1 non_negative=0 negative=-1 non_positive=0 even=2 love_for_pydantic=inf
"""



2 进阶应用

2.1 validator验证对象关系

from pydantic import BaseModel, ValidationError, validator


class UserModel(BaseModel):
    # 先规定UserModel必要的几个参数
    name: str
    username: str
    password1: str
    password2: str

	# 然后校验不同参数,validator()第一个参数name
    @validator('name')
    def name_must_contain_space(cls, v):
        if ' ' not in v:
            raise ValueError('must contain a space')
        return v.title()

    @validator('password2')
    def passwords_match(cls, v, values, **kwargs):
        if 'password1' in values and v != values['password1']:
            raise ValueError('passwords do not match')
        return v

    @validator('username')
    def username_alphanumeric(cls, v):
        assert v.isalnum(), 'must be alphanumeric'
        return v


user = UserModel(
    name='samuel colvin',
    username='scolvin',
    password1='zxcvbn',
    password2='zxcvbn',
)
print(user)
#> name='Samuel Colvin' username='scolvin' password1='zxcvbn' password2='zxcvbn'

try:
    UserModel(
        name='samuel',
        username='scolvin',
        password1='zxcvbn',
        password2='zxcvbn2',
    )
except ValidationError as e:
    print(e)

再来举一个验证字符内容(验证输入的手机号码)的例子:

import re
from pydantic import BaseModel, validator, ValidationError
from typing import Optional


class Address(BaseModel):
    street: str
    number: int
    zipcode: str


class Person(BaseModel):
    first_name: str
    last_name: str
    cell_phone_number: str
    address: Optional[Address]

    @validator("cell_phone_number")
    def validate_cell_phone_number(cls, v):
        match = re.match(r"^135\d{8}$", v)
        if len(v) != 11:
            raise ValueError("cell phone number must be 11 digits")
        elif match is None:
            raise ValueError("cell phone number must start with 135")
        return v

这里验证手机号,可以看到re.match(r"^135\d{8}$", v)代表着,检查这串手机号,是否是135开头,以及后面是否是8位数。

2.2 验证pipeline:Annotated

可以一个数列连续验证多次,同时对数列进行一些操作,比如加减乘除

from typing import Any, List

from typing_extensions import Annotated

from pydantic import BaseModel, ValidationError
from pydantic.functional_validators import AfterValidator


def check_squares(v: int) -> int:
    assert v**0.5 % 1 == 0, f'{v} is not a square number'
    return v


def double(v: Any) -> Any:
    return v * 2

# 核心在Annotated这个函数
MyNumber = Annotated[int, AfterValidator(double), AfterValidator(check_squares)]


class DemoModel(BaseModel):
    number: List[MyNumber]


print(DemoModel(number=[2, 8]))
#> number=[4, 16]
try:
    DemoModel(number=[2, 4])
except ValidationError as e:
    print(e)
    """
    1 validation error for DemoModel
    number.1
      Assertion failed, 8 is not a square number
    assert ((8 ** 0.5) % 1) == 0 [type=assertion_error, input_value=4, input_type=int]
    """

核心解读一下这句话:

Annotated[int, AfterValidator(double), AfterValidator(check_squares)]

解读一下,

  • 先执行验证是否是整数,
  • 执行后再执行AfterValidator(double)此时double其实是一个函数,将[2,4]变成[4,16]
  • 再接着执行AfterValidator(check_squares),检查[4,16]是否都是平方数,4是2的平方,16是4的平方

2.3 如何多个字段处理nan值

如果模型中有多个字段需要处理 NaN 值,我们可以定义一个通用的验证器,并使用 each_item 参数应用于每个字段。这样,我们可以重用相同的验证逻辑,而不需要为每个字段单独编写验证器。

下面是一个例子,展示如何定义一个通用的 nan_to_none 验证器并将其应用于模型中的多个字段:

from pydantic import BaseModel, root_validator
from typing import Optional
import math

class MyModel(BaseModel):
    my_float_1: Optional[float] = None
    my_float_2: Optional[float] = None
    my_float_3: Optional[float] = None

    @root_validator(pre=True)  # 应用于所有字段
    def handle_nan_for_all(cls, values):
        for field, value in values.items():
            if isinstance(value, float) and math.isnan(value):
                values[field] = None
        return values

# 示例:使用 NaN 值,将被转换为 None
model = MyModel(my_float_1=float('nan'), my_float_2=2.0, my_float_3=float('nan'))
print(model)  # my_float_1=None my_float_2=2.0 my_float_3=None

在这个例子中,我们使用了 root_validator 而不是 validator。root_validator 装饰器用于在模型的所有字段上执行验证,pre=True 参数表示在任何其他验证之前执行。在 handle_nan_for_all 验证器中,我们遍历了 values 字典,这个字典包含了所有传入模型的字段值。如果任何字段的值是 float 类型并且是 NaN,我们就将其替换为 None。

这种方法允许我们一次性处理模型中所有需要考虑 NaN 值的字段,而不需要为每个字段单独定义验证器。这样可以简化代码并减少重复。


参考文献

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐