Pydantic 初探-有哪些常见写法
- Authors
- @SLIPPERTOPIA
我之前写了一个 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_name
、admin_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
到本文撰写完的次日,即 2024-11-09,Gemini 终于 is now accessible from the OpenAI Library 🙏。 ↩