Skip to content

附录:练习题参考答案 ​

建议先自己动手做,再对照答案。答案不是唯一解,只要校验行为正确、代码清晰,就是好答案。

练习 1 参考答案:Book 模型 ​

python
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 参考答案:注册表单 ​

python
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 参考答案:订单与自动总价 ​

python
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:

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"}
]

清洗脚本:

python
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 参考答案:泛型统一响应 ​

python
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第八、十章(序列化、泛型)