WheatField
WheatField

Pydantic 初探-有哪些常见写法

November 9, 20241511 words, 8 min read
Authors

我之前写了一个 LLM API 网关(llmproxy) 的工具,把不同 LLM 的 API 统一成 OpenAI 的 API 格式。 这个项目真是费了很大功夫,有些平台比较亲和,即然当前 OpenAI 的 API 规范比较流行,那就兼容吧,比如 deepseek、groq。 也有一些不同平台就比较奇葩,非得搞一套自己的生态(说的就是你,Google Vertex AI 1),API 格式都不一样,input, output 都需要进行转换。

既然写 API,就少不了数据验证。多数情况下,我直接使用 FastAPI 的依赖库 Pydantic 进行数据验证。 定义好模型,然后在入参里规定好类型就 OK 了。调用时如果缺字段或类型不对,返回结果里也会有提示,至于出了什么错,该怎么解决,调用者你就自己慢慢研究吧。

Pydantic 用的越多,发现自己了解的越少。其实我很早之前就接触过 Pydantic 了,然而一直是把它当成一个加强版的 dataclass 来使用。 写了这么久 pydantic,上手还都是一招简单的 class AAA(BaseModel): ...,以至于每次看到 cursor 的代码提示,都只能感叹:“W 了个 C,居然还能这么写?😯”。

今日就痛定思痛,从 Netflix 里挤点时间,研究(罗列)一下 Pydantic 的一些常见写法。

p.s. 《ACANE II》今日上线 Netflix

基础数据验证

最常见也是最基础的用法,就是定义一个模型,用来进行数据验证,比如规范化字段长度、类型等。

from pydantic import BaseModel, Field, ValidationError

class User(BaseModel):
    name: str = Field(..., min_length=2, max_length=50)
    age: int = Field(..., ge=0, le=150)
    email: str
    is_active: bool = True  # 默认值

    # 可选字段
    nickname: str | None = None  # Python 3.10+
    # 或
    # nickname: Optional[str] = None  # Python 3.9 及以下

try:
    user = User(name="A", age=200, email="invalid")
except ValidationError as e:
    print(e.errors())

返回结果

[{'type': 'string_too_short', 'loc': ('name',), 'msg': 'String should have at least 2 characters', 'input': 'A', 'ctx': {'min_length': 2}, 'url': 'https://errors.pydantic.dev/2.9/v/string_too_short'}, {'type': 'less_than_equal', 'loc': ('age',), 'msg': 'Input should be less than or equal to 150', 'input': 200, 'ctx': {'le': 150}, 'url': 'https://errors.pydantic.dev/2.9/v/less_than_equal'}]

结果提示两个字段错误:name 长度小于 2,age 大于 150。

但也可以看到,尽管输入中 email = invalid,但运行时没有提示错误。 因为 email 字段只规定了类型(str),没有规定格式(邮箱格式),这样就挡不住用户在注册时小手一抖随便写一个abracadabra

复杂验证

如果要验证邮箱格式,可以直接使用内置的 EmailStr 类型,也可以通过自定义验证器。

如下例中所示,通过自定义验证器,可以对字段进行任意形式的验证。比如,密码是否至少包含一个数字、大小写字母等。

from pydantic import BaseModel, validator, EmailStr, constr

class UserProfile(BaseModel):
    username: constr(min_length=3, max_length=50)  # 约束字符串
    email: EmailStr  # 专门的邮箱验证
    password: str
    confirm_password: str

    # 类方法验证器
    @validator('username')
    def username_alphanumeric(cls, v):
        if not v.isalnum():
            raise ValueError('must be alphanumeric')
        return v

    # 依赖其他字段的验证
    @validator('confirm_password')
    def passwords_match(cls, v, values, **kwargs):
        if 'password' in values and v != values['password']:
            raise ValueError('passwords do not match')
        return v

    # 多字段验证器
    @validator('*')
    def no_whitespace(cls, v, field):
        if isinstance(v, str) and ' ' in v:
            raise ValueError(f'{field.name} cannot contain whitespace')
        return v

嵌套模型

对于比较复杂的数据结构,比如一个用户有多个地址,每个地址有多个标签,就可以使用嵌套模型,即通过递归验证和模型组合来实现。

from typing import List, Dict

class Address(BaseModel):
    street: str
    city: str
    country: str

class Tag(BaseModel):
    name: str
    value: str

class User(BaseModel):
    name: str
    addresses: List[Address]  # 列表嵌套
    tags: Dict[str, Tag]     # 字典嵌套

# 使用
user = User(
    name="John",
    addresses=[
        {"street": "123 Main St", "city": "NY", "country": "USA"},
        {"street": "456 Side St", "city": "LA", "country": "USA"}
    ],
    tags={
        "primary": {"name": "type", "value": "customer"},
        "secondary": {"name": "status", "value": "active"}
    }
)

类型转换与特殊类型

Pydantic 可以自动进行类型转换,比如将字符串转换为浮点数、整数、日期时间等。 比较适用的一种场景是,上游直接传入 json 字符串,下游解析后丢失了类型,需要自动进行类型转换。

from datetime import datetime
from pydantic import BaseModel, HttpUrl, conint, confloat

class Product(BaseModel):
    name: str
    price: confloat(gt=0)  # 大于0的浮点数
    quantity: conint(ge=0)  # 大于等于0的整数
    url: HttpUrl  # URL验证
    created_at: datetime  # 自动解析日期时间
    tags: set[str]  # 集合类型

# 自动类型转换
product = Product(
    name="Phone",
    price="99.99",        # 字符串转浮点数
    quantity="5",         # 字符串转整数
    url="https://example.com/product",
    created_at="2023-01-01T12:00:00",  # 字符串转datetime
    tags=["electronics", "gadget", "electronics"]  # 列表转集合(自动去重)
)

配置与环境变量

Pydantic 的 BaseSettings 类可以方便地从环境变量中加载配置,是 dotenv 的另一个选择。 但要注意的是,BaseSettings 类会自动从环境变量中加载配置,给对应字段(app_nameadmin_email 等)赋值,所以需要提前设置好环境变量。如果环境变量中没有设置,就需要通过 field(default=...) 指定默认值,或者将对应字段设置为 Optional 类型。

from pydantic import BaseSettings

class Settings(BaseSettings):
    app_name: str
    admin_email: str
    database_url: str
    api_key: str
    debug: bool = False

    class Config:
        env_file = '.env'  # 从.env文件加载
        env_file_encoding = 'utf-8'

# 自动从环境变量加载配置
settings = Settings()

数据导出与序列化

Pydantic 可以方便地将数据导出为字典、JSON 等格式,也可以进行自定义序列化。

class User(BaseModel):
    name: str
    age: int

    class Config:
        # 额外序列化配置
        json_encoders = {
            datetime: lambda v: v.strftime("%Y-%m-%d")
        }

user = User(name="John", age=30)

# 不同格式导出
dict_data = user.dict()           # 字典
json_data = user.json()           # JSON
json_pretty = user.json(indent=2) # 格式化JSON
exclude = user.dict(exclude={'age'})  # 排除字段
include = user.dict(include={'name'}) # 包含字段

计算字段

通过 @computed_field 装饰器,可以方便地定义计算字段。

from pydantic import BaseModel, computed_field

class Rectangle(BaseModel):
    width: float
    height: float

    @computed_field
    def area(self) -> float:
        return self.width * self.height

小结

这里只罗列了 Pydantic 的一些常见写法,Pydantic 的功能当然不止这些,还有很多高级用法,后面得空会继续补充。

Footnotes

  1. 到本文撰写完的次日,即 2024-11-09,Gemini 终于 is now accessible from the OpenAI Library 🙏。