【Pydantic】BaseModel 使用教程与实践

引言

在 Python 项目开发中,数据校验是保障程序稳定性的核心环节。传统方式依赖大量 if-else 条件判断,不仅代码冗余、可读性差,还容易出现校验遗漏、规则不统一等问题。尤其在接口开发、数据解析、业务参数校验场景中,这些问题极易引发线上 BUG。

Pydantic 是 Python 生态中主流的数据校验库,依托 Python 类型提示机制,能够快速实现结构化数据校验、类型自动转换、异常精准捕获。它已成为 FastAPI、LangChain、Haystack 等框架的底层数据基础。

本文核心内容:

  • Pydantic 的核心概念与基础用法
  • 字段约束与自定义校验器
  • 嵌套模型与复杂数据结构
  • 数据验证方法(model_validate / model_validate_json
  • 实战场景与最佳实践
  • 常见问题与避坑指南

本文基于 Pydantic v2 版本,v2 相较于 v1 性能提升数倍,语法更简洁,是当前生产环境的主流版本。


一、Pydantic 是什么?

1.1 核心概念

Pydantic 是一个基于 Python 类型提示的数据验证库。它的核心理念是:

用 Python 的类型注解定义数据结构,Pydantic 自动负责验证、转换和序列化。

当你定义一个 Pydantic 模型时,实际上是声明了数据的"形状"——字段名称、类型、约束条件。当真实数据传入时,Pydantic 会:

  1. 验证:检查数据是否符合类型和约束
  2. 转换:自动将数据转换为声明类型(如字符串 "123" 转为整数 123
  3. 报错:如果验证失败,抛出详细的 ValidationError

1.2 为什么选择 Pydantic?

优势 说明
类型安全 基于 Python 类型提示,与 IDE 和类型检查器完美集成
自动验证 声明即校验,无需编写大量 if-else
自动类型转换 宽松模式自动转换兼容类型,如字符串转整数
详细错误信息 验证失败时提供精确的错误定位和原因
序列化支持 一键导出字典或 JSON(model_dump / model_dump_json
高性能 v2 基于 Rust 编写的 pydantic-core,性能提升显著

二、快速上手:第一个 Pydantic 模型

2.1 安装

pip install pydantic

2.2 定义模型

from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str
    email: str
    age: int = 18  # 带默认值,可选

2.3 创建实例

# 正常创建
user = User(id=1, name="张三", email="zhangsan@example.com")
print(user)
# id=1 name='张三' email='zhangsan@example.com' age=18

# 自动类型转换:字符串 "2" 转为整数 2
user2 = User(id="2", name="李四", email="lisi@example.com")
print(user2.id)  # 2 (int)

# 验证失败:抛出 ValidationError
try:
    user3 = User(id="not_a_number", name="王五", email="wangwu@example.com")
except ValidationError as e:
    print(e)
    """
    1 validation error for User
    id
      Input should be a valid integer, unable to parse string as an integer
      [type=int_parsing, input_value='not_a_number', input_type=str]
    """

三、模型与字段详解

3.1 基础模型定义

Pydantic 模型的核心就是继承 BaseModel,用类型注解声明字段:

from pydantic import BaseModel

class Address(BaseModel):
    street: str
    city: str
    state: str
    zip_code: str
    country: str = "US"           # 可选,默认 "US"
    apartment: str | None = None  # 可选,默认 None

addr = Address(
    street="123 Main St",
    city="Springfield",
    state="IL",
    zip_code="62704",
)
print(addr)
# street='123 Main St' city='Springfield' state='IL' zip_code='62704' country='US' apartment=None

规则说明:

  • 无默认值字段:必填
  • 有默认值字段:可选
  • 类型为 T | None 且默认 None:可选

3.2 Field 字段约束

当简单类型注解不足以满足需求时,使用 Field 添加约束:

from pydantic import BaseModel, Field

class Product(BaseModel):
    name: str = Field(
        min_length=1,
        max_length=200,
        title="Product Name",
        description="商品显示名称",
        examples=["Widget Pro"]
    )
    sku: str = Field(
        pattern=r"^[A-Z]{2,4}-\d{4,8}$",
        description="库存单位,格式 'XX-0000'"
    )
    price: float = Field(
        gt=0,
        le=999_999.99,
        description="美元价格,必须为正"
    )
    quantity: int = Field(
        default=0,
        ge=0,
        description="库存数量,不可为负"
    )

常用 Field 参数:

参数 说明
min_length / max_length 字符串长度限制
pattern 正则表达式校验
gt / ge 大于 / 大于等于(数值)
lt / le 小于 / 小于等于(数值)
default 默认值
title / description 元数据(用于文档生成)
examples 示例值
validation_alias 输入时的字段别名

3.3 字段别名

当输入字段名与模型字段名不一致时,使用 validation_alias

from pydantic import BaseModel, Field, ConfigDict

class Product(BaseModel):
    model_config = ConfigDict(populate_by_name=True)  # 允许同时使用字段名和别名
    
    name: str = Field(validation_alias="product_name")
    category: str = Field(validation_alias="product_category")

# 使用别名传入
product = Product(product_name="Widget", product_category="Electronics")
print(product.name)   # Widget
print(product.category)  # Electronics

# 也可以使用字段名(由于 populate_by_name=True)
product2 = Product(name="Gadget", category="Tools")

四、数据验证方法

Pydantic 提供了多种验证方法,适用于不同数据来源:

4.1 model_validate():字典/对象验证

from datetime import datetime
from pydantic import BaseModel, ValidationError

class User(BaseModel):
    id: int
    name: str
    signup_ts: datetime | None = None

# 从字典创建
user = User.model_validate({'id': 123, 'name': 'James'})
print(user)
# id=123 name='James' signup_ts=None

# 验证失败
try:
    User.model_validate(['not', 'a', 'dict'])
except ValidationError as e:
    print(e)
    # Input should be a valid dictionary or instance of User

4.2 model_validate_json():JSON 验证

# 从 JSON 字符串创建(性能更好)
m = User.model_validate_json('{"id": 123, "name": "James"}')
print(m)
# id=123 name='James' signup_ts=None

# 无效 JSON
try:
    User.model_validate_json('invalid JSON')
except ValidationError as e:
    print(e)
    # expected value at line 1 column 1

4.3 三种验证方法对比

方法 输入类型 适用场景
model_validate() 字典或对象 从 Python 数据结构创建
model_validate_json() JSON 字符串或 bytes 从 API 响应、文件读取
model_validate_strings() 字符串键值字典 从表单数据、环境变量

五、自定义校验器

5.1 @field_validator:字段级校验

当基础类型约束无法满足业务需求时,编写自定义校验器:

from pydantic import BaseModel, field_validator
import re

class User(BaseModel):
    username: str
    phone: str
    email: str

    @field_validator('username', mode='after')
    @classmethod
    def validate_username(cls, value: str) -> str:
        """用户名:4-20个字符,仅字母数字和下划线"""
        if not re.match(r'^[a-zA-Z0-9_]{4,20}$', value):
            raise ValueError('用户名必须为4-20个字母、数字或下划线')
        return value

    @field_validator('phone', mode='after')
    @classmethod
    def validate_phone(cls, value: str) -> str:
        """手机号:11位数字"""
        if not re.match(r'^\d{11}$', value):
            raise ValueError('手机号必须为11位数字')
        return value

# 测试
try:
    user = User(username="ab", phone="123", email="test@example.com")
except ValidationError as e:
    print(e)

5.2 @model_validator:模型级校验

跨字段校验(如密码一致性)使用模型级校验器:

from pydantic import BaseModel, model_validator
from typing_extensions import Self

class UserRegister(BaseModel):
    username: str
    password: str
    password_repeat: str

    @model_validator(mode='after')
    def check_passwords_match(self) -> Self:
        if self.password != self.password_repeat:
            raise ValueError('两次输入的密码不一致')
        return self

# 测试
try:
    user = UserRegister(
        username="alice",
        password="secret123",
        password_repeat="secret456"
    )
except ValidationError as e:
    print(e)
    # 1 validation error for UserRegister
    #   Value error, 两次输入的密码不一致

5.3 校验器模式说明

模式 说明
mode='before' 在 Pydantic 处理前执行,接收原始输入
mode='after' 在 Pydantic 处理后执行,接收已转换的值
mode='wrap' 最灵活,可控制验证流程,执行前后逻辑

六、嵌套模型与复杂数据结构

6.1 列表与嵌套模型

真实数据往往是嵌套结构,Pydantic 支持模型嵌套:

from pydantic import BaseModel, Field
from typing import List, Literal

class Address(BaseModel):
    street: str = Field(min_length=3, max_length=50)
    city: str = Field(min_length=2, max_length=30)
    zip_code: str = Field(pattern=r"^\d{5}$")
    type: Literal["home", "work"] = Field()

class User(BaseModel):
    id: int = Field(default=1, gt=0)
    name: str = Field(default="someName", min_length=1)
    addresses: List[Address] = Field(min_length=1)  # 至少一个地址

# 创建包含嵌套模型的实例
user_data = {
    "id": 1,
    "name": "Alice",
    "addresses": [
        {
            "street": "123 Main St",
            "city": "New York",
            "zip_code": "10001",
            "type": "home"
        }
    ]
}

user = User.model_validate(user_data)
print(user)
# id=1 name='Alice' addresses=[Address(street='123 Main St', city='New York', zip_code='10001', type='home')]

6.2 泛型模型

复用通用模型结构时,使用泛型模型:

from typing import Generic, List, TypeVar
from pydantic import BaseModel

DataT = TypeVar('DataT')

class Response(BaseModel, Generic[DataT]):
    data: DataT | None = None
    code: int = 200
    message: str = "success"

# 不同类型的数据
print(Response[int](data=1))
# data=1 code=200 message='success'

print(Response[str](data='value'))
# data='value' code=200 message='success'

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

print(Response[User](data=User(id=1, name="Alice")))
# data=User(id=1, name='Alice') code=200 message='success'

七、实战场景

7.1 FastAPI 请求验证

FastAPI 深度集成 Pydantic,直接用模型定义请求体:

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class UserCreate(BaseModel):
    username: str = Field(min_length=3, max_length=20)
    email: str = Field(pattern=r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
    password: str = Field(min_length=8)

@app.post("/users/")
def create_user(user: UserCreate):
    # 自动验证,无需手动校验
    return {"username": user.username, "email": user.email}

7.2 LLM 输出验证

在 RAG 项目中,使用 Pydantic 验证 LLM 结构化输出:

from pydantic import BaseModel, Field, field_validator
from typing import List
import json
import re

class ProductReview(BaseModel):
    product_name: str
    rating: int = Field(ge=1, le=5)
    review_text: str
    would_recommend: bool

def parse_review(llm_output: str) -> ProductReview | None:
    """解析 LLM 输出并验证"""
    try:
        # 提取 JSON
        json_match = re.search(r'\{.*\}', llm_output, re.DOTALL)
        if not json_match:
            return None
        data = json.loads(json_match.group())
        return ProductReview(**data)
    except (json.JSONDecodeError, ValidationError) as e:
        print(f"解析失败: {e}")
        return None

# 处理包含多余文本的 LLM 响应
messy_response = '''
Here's the review in JSON format:

{
    "product_name": "Wireless Headphones X100",
    "rating": 4,
    "review_text": "Great sound quality, comfortable for long use.",
    "would_recommend": true
}

Hope this helps!
'''

review = parse_review(messy_response)
if review:
    print(f"Product: {review.product_name}")
    print(f"Rating: {review.rating}/5")

7.3 配置文件管理

from pydantic import BaseModel, Field
import json
import os

class DatabaseConfig(BaseModel):
    host: str = "localhost"
    port: int = Field(default=5432, ge=1, le=65535)
    username: str
    password: str
    database: str

class AppConfig(BaseModel):
    app_name: str
    debug: bool = False
    database: DatabaseConfig

def load_config(config_path: str) -> AppConfig:
    with open(config_path) as f:
        data = json.load(f)
    return AppConfig.model_validate(data)

# config.json
# {
#   "app_name": "MyApp",
#   "database": {
#     "host": "localhost",
#     "username": "admin",
#     "password": "secret",
#     "database": "mydb"
#   }
# }

八、常见问题与避坑指南

8.1 问题1:参数不可哈希

lru_cache 要求参数可哈希,Pydantic 模型默认不可哈希。解决方案:

@lru_cache
def get_user_by_id(user_id: int):  # ✅ 使用简单类型
    return db.query(User, user_id)

# 或者将模型转为可哈希
@lru_cache
def process_user(user_tuple: tuple):  # 传元组
    user = User.model_validate(dict(user_tuple))
    # ...

8.2 问题2:大模型无限递归

使用 model_dump() 时注意循环引用:

# ❌ 循环引用可能导致递归
user = User(name="Alice")
user.friend = user
user.model_dump()  # RecursionError

# ✅ 使用 model_dump(exclude={'friend'})

8.3 问题3:性能优化

  • 使用 model_validate_json() 直接解析 JSON,比先解析为字典再验证更快
  • 使用 model_construct() 跳过验证,只用于已确认有效的数据
  • 设置 ConfigDict(revalidate_instances='never') 避免重复验证实例

九、总结

核心概念 说明
BaseModel 所有 Pydantic 模型的基类
Field 添加字段约束和元数据
@field_validator 字段级自定义校验
@model_validator 模型级跨字段校验
model_validate() 从字典/对象创建并验证
model_validate_json() 从 JSON 创建并验证(性能更优)
model_dump() 导出为字典
model_dump_json() 导出为 JSON 字符串

什么时候用 Pydantic?

推荐使用:

  • FastAPI 请求/响应验证
  • 配置文件解析
  • API 客户端数据解析
  • LLM 输出结构化验证
  • 数据库模型(配合 SQLAlchemy)
  • 环境变量管理(pydantic-settings

不推荐:

  • 极致性能场景(但 v2 已显著优化)
  • 极其动态、运行时才确定的结构

扩展阅读:

posted @ 2026-08-15 14:50  静心笃行。  阅读(2)  评论(0)    收藏  举报