目錄
02-4. 巢狀模型與繼承
⏱️ 閱讀時間: 12 分鐘 🎯 難度: ⭐⭐ (基礎)
🤔 一句話解釋
巢狀模型讓你組合複雜的資料結構,繼承讓你重用共同的欄位定義。
🏠 巢狀模型
基本巢狀
from pydantic import BaseModel
from typing import Optional
class Address(BaseModel):
"""地址"""
city: str
street: str
zip_code: str
class User(BaseModel):
"""使用者(包含地址)"""
name: str
email: str
address: Address # 巢狀模型
# 使用
user = User(
name="John",
email="john@example.com",
address={
"city": "台北市",
"street": "信義路100號",
"zip_code": "110"
}
)
# 存取巢狀屬性
print(user.address.city) # 台北市選填的巢狀模型
from pydantic import BaseModel
from typing import Optional
class Address(BaseModel):
city: str
street: str
class Company(BaseModel):
name: str
website: Optional[str] = None
class User(BaseModel):
name: str
email: str
address: Optional[Address] = None # 整個地址可選
company: Optional[Company] = None # 整個公司可選
# 可以不提供巢狀模型
user = User(name="John", email="john@example.com")
print(user.address) # None巢狀列表
from pydantic import BaseModel
from typing import List
class Tag(BaseModel):
name: str
color: str = "gray"
class Image(BaseModel):
url: str
alt: str = ""
width: Optional[int] = None
height: Optional[int] = None
class Product(BaseModel):
name: str
price: float
tags: List[Tag] = [] # Tag 列表
images: List[Image] = [] # Image 列表
# 使用
product = Product(
name="iPhone 15",
price=35900,
tags=[
{"name": "手機", "color": "blue"},
{"name": "Apple", "color": "gray"},
],
images=[
{"url": "https://example.com/1.jpg", "alt": "正面"},
{"url": "https://example.com/2.jpg", "alt": "背面"},
]
)
# 存取
for tag in product.tags:
print(f"{tag.name}: {tag.color}")深層巢狀
from pydantic import BaseModel
from typing import List, Optional
class Street(BaseModel):
name: str
number: str
class City(BaseModel):
name: str
country: str
class Address(BaseModel):
street: Street
city: City
zip_code: str
class Contact(BaseModel):
type: str # "home", "work"
address: Address
class Person(BaseModel):
name: str
contacts: List[Contact]
# 使用
person = Person(
name="John",
contacts=[
{
"type": "home",
"address": {
"street": {"name": "Main St", "number": "123"},
"city": {"name": "New York", "country": "USA"},
"zip_code": "10001"
}
}
]
)
# 深層存取
print(person.contacts[0].address.city.name) # New York🧬 模型繼承
基本繼承
from pydantic import BaseModel
from datetime import datetime
class BaseEntity(BaseModel):
"""基礎實體(包含 ID 和時間戳)"""
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class User(BaseEntity):
"""使用者(繼承基礎實體)"""
name: str
email: str
class Product(BaseEntity):
"""商品(繼承基礎實體)"""
name: str
price: float
# User 和 Product 都有 id, created_at, updated_at請求/回應模型的繼承模式
from pydantic import BaseModel, Field, EmailStr, ConfigDict
from typing import Optional
from datetime import datetime
# ===== 基礎 =====
class UserBase(BaseModel):
"""使用者基礎欄位(共用)"""
email: EmailStr
username: str = Field(min_length=3, max_length=50)
full_name: Optional[str] = None
# ===== 建立 =====
class UserCreate(UserBase):
"""建立使用者(需要密碼)"""
password: str = Field(min_length=8)
# ===== 更新 =====
class UserUpdate(BaseModel):
"""更新使用者(全部選填)"""
email: Optional[EmailStr] = None
username: Optional[str] = Field(None, min_length=3, max_length=50)
full_name: Optional[str] = None
password: Optional[str] = Field(None, min_length=8)
# ===== 回應 =====
class UserResponse(UserBase):
"""使用者回應(不含密碼)"""
id: int
is_active: bool
created_at: datetime
model_config = ConfigDict(from_attributes=True)
# ===== 完整(含密碼雜湊,內部使用)=====
class UserInDB(UserResponse):
"""資料庫中的使用者"""
hashed_password: str多重繼承
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional
# Mixin 類別
class TimestampMixin(BaseModel):
"""時間戳記"""
created_at: datetime
updated_at: Optional[datetime] = None
class SoftDeleteMixin(BaseModel):
"""軟刪除"""
is_deleted: bool = False
deleted_at: Optional[datetime] = None
class AuditMixin(BaseModel):
"""稽核"""
created_by: Optional[int] = None
updated_by: Optional[int] = None
# 組合使用
class Article(TimestampMixin, SoftDeleteMixin, AuditMixin):
"""文章(組合多個 Mixin)"""
id: int
title: str
content: str
author_id: int
# Article 現在有所有 Mixin 的欄位覆寫父類別欄位
from pydantic import BaseModel, Field
class BaseUser(BaseModel):
name: str = Field(max_length=50)
age: int = Field(ge=0)
class StrictUser(BaseUser):
# 覆寫父類別的欄位,使用更嚴格的驗證
name: str = Field(min_length=2, max_length=30)
age: int = Field(ge=18, le=100) # 必須成年
class AdminUser(BaseUser):
# 新增欄位
role: str = "admin"
permissions: list[str] = []🔄 泛型模型
基本泛型
from pydantic import BaseModel
from typing import TypeVar, Generic, List
T = TypeVar('T')
class PaginatedResponse(BaseModel, Generic[T]):
"""分頁回應(泛型)"""
items: List[T]
total: int
page: int
page_size: int
pages: int
class User(BaseModel):
id: int
name: str
class Product(BaseModel):
id: int
name: str
price: float
# 使用泛型
user_page: PaginatedResponse[User] = PaginatedResponse(
items=[User(id=1, name="John")],
total=1,
page=1,
page_size=10,
pages=1
)
product_page: PaginatedResponse[Product] = PaginatedResponse(
items=[Product(id=1, name="iPhone", price=999)],
total=1,
page=1,
page_size=10,
pages=1
)API 回應包裝器
from pydantic import BaseModel
from typing import TypeVar, Generic, Optional
from datetime import datetime
T = TypeVar('T')
class APIResponse(BaseModel, Generic[T]):
"""統一 API 回應格式"""
success: bool = True
data: Optional[T] = None
message: Optional[str] = None
timestamp: datetime = Field(default_factory=datetime.now)
class ErrorResponse(BaseModel):
"""錯誤回應"""
success: bool = False
error_code: str
message: str
details: Optional[dict] = None
timestamp: datetime = Field(default_factory=datetime.now)
# 使用
class UserData(BaseModel):
id: int
name: str
response: APIResponse[UserData] = APIResponse(
data=UserData(id=1, name="John"),
message="使用者建立成功"
)📝 實戰範例:電商系統
from pydantic import BaseModel, Field, EmailStr, ConfigDict
from typing import List, Optional
from datetime import datetime
from decimal import Decimal
from enum import Enum
# ===== Enums =====
class OrderStatus(str, Enum):
PENDING = "pending"
PAID = "paid"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
class PaymentMethod(str, Enum):
CREDIT_CARD = "credit_card"
BANK_TRANSFER = "bank_transfer"
COD = "cod"
# ===== 基礎模型 =====
class TimestampMixin(BaseModel):
created_at: datetime
updated_at: Optional[datetime] = None
# ===== 地址 =====
class AddressBase(BaseModel):
recipient: str = Field(min_length=1, max_length=100)
phone: str = Field(pattern=r"^09\d{8}$")
city: str
district: str
street: str
zip_code: str = Field(pattern=r"^\d{3,5}$")
is_default: bool = False
class AddressCreate(AddressBase):
pass
class AddressResponse(AddressBase):
id: int
model_config = ConfigDict(from_attributes=True)
# ===== 商品 =====
class ProductBase(BaseModel):
name: str = Field(min_length=1, max_length=200)
description: Optional[str] = None
price: Decimal = Field(ge=0, decimal_places=2)
stock: int = Field(ge=0)
class ProductCreate(ProductBase):
category_id: int
class ProductResponse(ProductBase, TimestampMixin):
id: int
category_id: int
is_active: bool
model_config = ConfigDict(from_attributes=True)
# ===== 訂單項目 =====
class OrderItemBase(BaseModel):
product_id: int
quantity: int = Field(ge=1)
unit_price: Decimal
class OrderItemCreate(BaseModel):
product_id: int
quantity: int = Field(ge=1)
class OrderItemResponse(OrderItemBase):
id: int
product: ProductResponse # 巢狀商品資訊
@property
def subtotal(self) -> Decimal:
return self.unit_price * self.quantity
model_config = ConfigDict(from_attributes=True)
# ===== 訂單 =====
class OrderBase(BaseModel):
shipping_address_id: int
payment_method: PaymentMethod
note: Optional[str] = Field(None, max_length=500)
class OrderCreate(OrderBase):
items: List[OrderItemCreate] = Field(min_length=1)
coupon_code: Optional[str] = None
class OrderResponse(OrderBase, TimestampMixin):
id: int
order_number: str
status: OrderStatus
items: List[OrderItemResponse]
shipping_address: AddressResponse
subtotal: Decimal
shipping_fee: Decimal
discount: Decimal
total: Decimal
paid_at: Optional[datetime] = None
shipped_at: Optional[datetime] = None
delivered_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
# ===== 使用者 =====
class UserBase(BaseModel):
email: EmailStr
username: str = Field(min_length=3, max_length=50)
full_name: Optional[str] = None
phone: Optional[str] = Field(None, pattern=r"^09\d{8}$")
class UserCreate(UserBase):
password: str = Field(min_length=8)
class UserResponse(UserBase, TimestampMixin):
id: int
is_active: bool
addresses: List[AddressResponse] = []
model_config = ConfigDict(from_attributes=True)
class UserWithOrders(UserResponse):
"""使用者(含訂單歷史)"""
orders: List[OrderResponse] = []✅ 重點總結
巢狀模型
class Inner(BaseModel):
value: str
class Outer(BaseModel):
inner: Inner # 必填巢狀
optional: Optional[Inner] = None # 選填巢狀
items: List[Inner] = [] # 巢狀列表繼承模式
# 基礎欄位
class Base(BaseModel):
common_field: str
# 建立用(加密碼)
class Create(Base):
password: str
# 更新用(全選填)
class Update(BaseModel):
field: Optional[str] = None
# 回應用(加 ID、時間)
class Response(Base):
id: int
created_at: datetime
model_config = ConfigDict(from_attributes=True)泛型
from typing import TypeVar, Generic
T = TypeVar('T')
class Wrapper(BaseModel, Generic[T]):
data: T
# 使用
Wrapper[User](data=user)🎤 面試這樣答
Q: 為什麼要把 Create、Update、Response 分成不同的模型?
答案:
因為不同操作需要不同的欄位:
- Create:需要所有必填欄位,可能包含密碼
- Update:所有欄位都是選填,只更新提供的欄位
- Response:包含 ID、時間戳,但不包含敏感資料如密碼
分離模型可以:
- 更安全(不會不小心回傳密碼)
- 更清晰(每個操作的需求一目了然)
- 更好的文件(Swagger 會顯示正確的欄位)
🤓 小測驗
如何讓巢狀模型變成選填?
model_config = ConfigDict(from_attributes=True)的作用?如何建立可重用的分頁回應模型?
上一篇: 02-3. 自訂驗證器 下一篇: 02-5. 序列化與反序列化
最後更新:2025-12-17