附录:练习题参考答案
建议先自己动手做,再对照答案。答案不是唯一解,只要校验行为正确、代码清晰,就是好答案。
练习 1 参考答案:Book 模型
from pydantic import BaseModel, Field, ValidationError
class Book(BaseModel):
title: str = Field(min_length=1, max_length=100)
author: str
price: float = Field(gt=0)
isbn: str = Field(pattern=r"^\d{13}$") # 13 位数字
# ✅ 正确数据
book = Book(title="Python 编程", author="张三", price=59.9, isbn="9787115428028")
print(book)
# ❌ 错误数据:价格负数、ISBN 位数不对
try:
Book(title="坏书", author="李四", price=-9.9, isbn="123")
except ValidationError as e:
print(e.errors())
# 能看到两条错误:price 的 gt 校验失败、isbn 的 pattern 校验失败考点回顾:Field(gt=0) 做数值约束、pattern 做格式约束、e.errors() 返回结构化错误列表。
练习 2 参考答案:注册表单
from pydantic import BaseModel, EmailStr, field_validator, model_validator
class RegisterForm(BaseModel):
email: EmailStr
password: str
confirm_password: str
@field_validator("email", mode="after")
@classmethod
def email_to_lower(cls, v: EmailStr) -> str:
return str(v).lower()
@field_validator("password")
@classmethod
def password_strength(cls, v: str) -> str:
if len(v) < 8:
raise ValueError("密码至少 8 位")
if not any(ch.isdigit() for ch in v):
raise ValueError("密码必须包含数字")
return v
@model_validator(mode="after")
def passwords_match(self):
if self.password != self.confirm_password:
raise ValueError("两次输入的密码不一致")
return self
# ✅ 测试
form = RegisterForm(
email=" Test@Example.COM",
password="abc12345",
confirm_password="abc12345",
)
print(form.email) # test@example.com
# ❌ 密码不一致
try:
RegisterForm(email="a@b.com", password="abc12345", confirm_password="abc12346")
except ValueError as e:
print(e)考点回顾:字段验证器管单个字段,model_validator(mode="after") 管跨字段逻辑,验证器返回的值会替换原值(所以能用来清洗数据)。
💡 注意:email 转小写也可以直接用模型配置 str_to_lower=True,或者更简单地在 before 模式里处理——条条大路通罗马。
练习 3 参考答案:订单与自动总价
from pydantic import BaseModel, Field, computed_field
class OrderItem(BaseModel):
product_name: str = Field(min_length=1)
unit_price: float = Field(gt=0)
quantity: int = Field(ge=1)
@computed_field
@property
def subtotal(self) -> float:
return round(self.unit_price * self.quantity, 2)
class Order(BaseModel):
order_no: str
items: list[OrderItem] = Field(min_length=1) # 至少一件商品
@computed_field
@property
def total_price(self) -> float:
return round(sum(item.subtotal for item in self.items), 2)
order = Order.model_validate({
"order_no": "SO20261010001",
"items": [
{"product_name": "键盘", "unit_price": 199.0, "quantity": 1},
{"product_name": "鼠标", "unit_price": 99.5, "quantity": 2},
],
})
print(order.total_price) # 398.0
print(order.model_dump_json())
# items 里每项会带 subtotal,订单会带 total_price —— computed_field 自动出现在输出里考点回顾:嵌套模型列表、computed_field 既能在代码里当属性用,又会出现在 model_dump() 结果里。
⚠️ 真实项目里金额请改用 Decimal 而不是 float,这里为简化示例用了 float + round。
练习 4 参考答案:脏数据清洗脚本
先造一份脏数据 dirty_products.json:
[
{"name": "机械键盘", "price": "¥199.0", "created": "2026-10-01"},
{"name": "鼠标", "price": "99.5元", "created": "2026/10/02"},
{"name": "", "price": "¥50", "created": "2026-10-03"},
{"name": "显示器", "price": "free", "created": "2026-10-04"},
{"price": "¥88"},
{"name": "扩展坞", "price": "¥299", "created": "2026-10-05"}
]清洗脚本:
import json
from datetime import datetime
from pydantic import BaseModel, Field, ValidationError, field_validator
class ProductRow(BaseModel):
name: str = Field(min_length=1)
price: float = Field(gt=0)
created: datetime | None = None
@field_validator("price", mode="before")
@classmethod
def parse_price(cls, v):
if isinstance(v, str):
cleaned = v.replace("¥", "").replace("元", "").strip()
return float(cleaned) # 转换失败会抛 ValueError,被包成 ValidationError
return v
@field_validator("created", mode="before")
@classmethod
def parse_date(cls, v):
if isinstance(v, str):
v = v.replace("/", "-") # 兼容 2026/10/02 这种格式
return v
with open("dirty_products.json", encoding="utf-8") as f:
messy_data = json.load(f)
valid_rows, bad_rows = [], []
for i, raw in enumerate(messy_data):
try:
valid_rows.append(ProductRow.model_validate(raw))
except ValidationError as e:
reasons = [f"{'.'.join(str(x) for x in err['loc'])}: {err['msg']}"
for err in e.errors()]
bad_rows.append((i, raw, reasons))
print(f"✅ 成功 {len(valid_rows)} 条,❌ 失败 {len(bad_rows)} 条\n")
for row in valid_rows:
print(row.model_dump_json())
print("\n失败明细:")
for i, raw, reasons in bad_rows:
print(f" 第 {i} 行 {raw}")
for r in reasons:
print(f" - {r}")预期结果:前两条和最后一条清洗成功;空 name、"free" 价格、缺 name 的三条进入失败列表,并且每条都带具体原因。
考点回顾:mode="before" 在类型转换前做清洗、try/except ValidationError 实现"好数据进、坏数据记录"、err['loc'] 定位出错字段。
💡 批量校验也可以写成 TypeAdapter(list[ProductRow]),但那是一条不过全不过的"整批模式";要逐行容错,还是循环 + try/except 更合适。
练习 5 参考答案:泛型统一响应
from typing import Generic, TypeVar
from pydantic import BaseModel, Field, computed_field
# ---- 复用练习 3 的订单模型 ----
class OrderItem(BaseModel):
product_name: str
unit_price: float = Field(gt=0)
quantity: int = Field(ge=1)
class Order(BaseModel):
order_no: str
items: list[OrderItem]
@computed_field
@property
def total_price(self) -> float:
return round(sum(i.unit_price * i.quantity for i in self.items), 2)
# ---- 泛型响应外壳 ----
T = TypeVar("T")
class ApiResponse(BaseModel, Generic[T]):
code: int = 0
message: str = "ok"
data: T | None = None
# ---- 使用 ----
orders = [
Order(order_no="SO001", items=[
OrderItem(product_name="键盘", unit_price=199, quantity=1)
]),
Order(order_no="SO002", items=[
OrderItem(product_name="鼠标", unit_price=99.5, quantity=2),
OrderItem(product_name="鼠标垫", unit_price=19.9, quantity=1),
]),
]
resp = ApiResponse[list[Order]](data=orders)
print(resp.model_dump_json(indent=2))
# data 是订单数组,每个订单都带着 total_price
# 反过来:校验外部传来的响应
incoming = '{"code": 0, "message": "ok", "data": [{"order_no": "SO003", "items": [{"product_name": "显示器", "unit_price": 899, "quantity": 1}]}]}'
parsed = ApiResponse[list[Order]].model_validate_json(incoming)
print(parsed.data[0].total_price) # 899.0,类型是 Order,IDE 有完整提示考点回顾:Generic[T] + TypeVar 让外壳复用、内容强类型;ApiResponse[list[Order]] 说明泛型参数本身也可以是容器类型,校验一路递归到底。
完成标准自评
做完 5 道题后,回头看第 14.3 节的自测清单。如果某道题做得磕磕绊绊,说明对应章节需要回炉:
| 不顺利的练习 | 建议重读 |
|---|---|
| 练习 1 | 第三、四章(类型与 Field 约束) |
| 练习 2 | 第五章(验证器) |
| 练习 3 | 第六、九章(嵌套、computed_field) |
| 练习 4 | 第五、十二章(before 模式、脏数据清洗) |
| 练习 5 | 第八、十章(序列化、泛型) |