# 

# 02-4. 巢狀模型與繼承

&gt; ⏱️ **閱讀時間：** 12 分鐘
&gt; 🎯 **難度：** ⭐⭐ (基礎)

---

## 🤔 一句話解釋

**巢狀模型讓你組合複雜的資料結構，繼承讓你重用共同的欄位定義。**

---

## 🏠 巢狀模型

### 基本巢狀

```python
from pydantic import BaseModel
from typing import Optional

class Address(BaseModel):
    &#34;&#34;&#34;地址&#34;&#34;&#34;
    city: str
    street: str
    zip_code: str

class User(BaseModel):
    &#34;&#34;&#34;使用者（包含地址）&#34;&#34;&#34;
    name: str
    email: str
    address: Address  # 巢狀模型

# 使用
user = User(
    name=&#34;John&#34;,
    email=&#34;john@example.com&#34;,
    address={
        &#34;city&#34;: &#34;台北市&#34;,
        &#34;street&#34;: &#34;信義路100號&#34;,
        &#34;zip_code&#34;: &#34;110&#34;
    }
)

# 存取巢狀屬性
print(user.address.city)  # 台北市
```

### 選填的巢狀模型

```python
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=&#34;John&#34;, email=&#34;john@example.com&#34;)
print(user.address)  # None
```

### 巢狀列表

```python
from pydantic import BaseModel
from typing import List

class Tag(BaseModel):
    name: str
    color: str = &#34;gray&#34;

class Image(BaseModel):
    url: str
    alt: str = &#34;&#34;
    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=&#34;iPhone 15&#34;,
    price=35900,
    tags=[
        {&#34;name&#34;: &#34;手機&#34;, &#34;color&#34;: &#34;blue&#34;},
        {&#34;name&#34;: &#34;Apple&#34;, &#34;color&#34;: &#34;gray&#34;},
    ],
    images=[
        {&#34;url&#34;: &#34;https://example.com/1.jpg&#34;, &#34;alt&#34;: &#34;正面&#34;},
        {&#34;url&#34;: &#34;https://example.com/2.jpg&#34;, &#34;alt&#34;: &#34;背面&#34;},
    ]
)

# 存取
for tag in product.tags:
    print(f&#34;{tag.name}: {tag.color}&#34;)
```

### 深層巢狀

```python
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  # &#34;home&#34;, &#34;work&#34;
    address: Address

class Person(BaseModel):
    name: str
    contacts: List[Contact]

# 使用
person = Person(
    name=&#34;John&#34;,
    contacts=[
        {
            &#34;type&#34;: &#34;home&#34;,
            &#34;address&#34;: {
                &#34;street&#34;: {&#34;name&#34;: &#34;Main St&#34;, &#34;number&#34;: &#34;123&#34;},
                &#34;city&#34;: {&#34;name&#34;: &#34;New York&#34;, &#34;country&#34;: &#34;USA&#34;},
                &#34;zip_code&#34;: &#34;10001&#34;
            }
        }
    ]
)

# 深層存取
print(person.contacts[0].address.city.name)  # New York
```

---

## 🧬 模型繼承

### 基本繼承

```python
from pydantic import BaseModel
from datetime import datetime

class BaseEntity(BaseModel):
    &#34;&#34;&#34;基礎實體（包含 ID 和時間戳）&#34;&#34;&#34;
    id: int
    created_at: datetime
    updated_at: Optional[datetime] = None

class User(BaseEntity):
    &#34;&#34;&#34;使用者（繼承基礎實體）&#34;&#34;&#34;
    name: str
    email: str

class Product(BaseEntity):
    &#34;&#34;&#34;商品（繼承基礎實體）&#34;&#34;&#34;
    name: str
    price: float

# User 和 Product 都有 id, created_at, updated_at
```

### 請求/回應模型的繼承模式

```python
from pydantic import BaseModel, Field, EmailStr, ConfigDict
from typing import Optional
from datetime import datetime

# ===== 基礎 =====
class UserBase(BaseModel):
    &#34;&#34;&#34;使用者基礎欄位（共用）&#34;&#34;&#34;
    email: EmailStr
    username: str = Field(min_length=3, max_length=50)
    full_name: Optional[str] = None

# ===== 建立 =====
class UserCreate(UserBase):
    &#34;&#34;&#34;建立使用者（需要密碼）&#34;&#34;&#34;
    password: str = Field(min_length=8)

# ===== 更新 =====
class UserUpdate(BaseModel):
    &#34;&#34;&#34;更新使用者（全部選填）&#34;&#34;&#34;
    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):
    &#34;&#34;&#34;使用者回應（不含密碼）&#34;&#34;&#34;
    id: int
    is_active: bool
    created_at: datetime

    model_config = ConfigDict(from_attributes=True)

# ===== 完整（含密碼雜湊，內部使用）=====
class UserInDB(UserResponse):
    &#34;&#34;&#34;資料庫中的使用者&#34;&#34;&#34;
    hashed_password: str
```

### 多重繼承

```python
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional

# Mixin 類別
class TimestampMixin(BaseModel):
    &#34;&#34;&#34;時間戳記&#34;&#34;&#34;
    created_at: datetime
    updated_at: Optional[datetime] = None

class SoftDeleteMixin(BaseModel):
    &#34;&#34;&#34;軟刪除&#34;&#34;&#34;
    is_deleted: bool = False
    deleted_at: Optional[datetime] = None

class AuditMixin(BaseModel):
    &#34;&#34;&#34;稽核&#34;&#34;&#34;
    created_by: Optional[int] = None
    updated_by: Optional[int] = None

# 組合使用
class Article(TimestampMixin, SoftDeleteMixin, AuditMixin):
    &#34;&#34;&#34;文章（組合多個 Mixin）&#34;&#34;&#34;
    id: int
    title: str
    content: str
    author_id: int

# Article 現在有所有 Mixin 的欄位
```

### 覆寫父類別欄位

```python
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 = &#34;admin&#34;
    permissions: list[str] = []
```

---

## 🔄 泛型模型

### 基本泛型

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

T = TypeVar(&#39;T&#39;)

class PaginatedResponse(BaseModel, Generic[T]):
    &#34;&#34;&#34;分頁回應（泛型）&#34;&#34;&#34;
    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=&#34;John&#34;)],
    total=1,
    page=1,
    page_size=10,
    pages=1
)

product_page: PaginatedResponse[Product] = PaginatedResponse(
    items=[Product(id=1, name=&#34;iPhone&#34;, price=999)],
    total=1,
    page=1,
    page_size=10,
    pages=1
)
```

### API 回應包裝器

```python
from pydantic import BaseModel
from typing import TypeVar, Generic, Optional
from datetime import datetime

T = TypeVar(&#39;T&#39;)

class APIResponse(BaseModel, Generic[T]):
    &#34;&#34;&#34;統一 API 回應格式&#34;&#34;&#34;
    success: bool = True
    data: Optional[T] = None
    message: Optional[str] = None
    timestamp: datetime = Field(default_factory=datetime.now)

class ErrorResponse(BaseModel):
    &#34;&#34;&#34;錯誤回應&#34;&#34;&#34;
    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=&#34;John&#34;),
    message=&#34;使用者建立成功&#34;
)
```

---

## 📝 實戰範例：電商系統

```python
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 = &#34;pending&#34;
    PAID = &#34;paid&#34;
    SHIPPED = &#34;shipped&#34;
    DELIVERED = &#34;delivered&#34;
    CANCELLED = &#34;cancelled&#34;

class PaymentMethod(str, Enum):
    CREDIT_CARD = &#34;credit_card&#34;
    BANK_TRANSFER = &#34;bank_transfer&#34;
    COD = &#34;cod&#34;

# ===== 基礎模型 =====
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&#34;^09\d{8}$&#34;)
    city: str
    district: str
    street: str
    zip_code: str = Field(pattern=r&#34;^\d{3,5}$&#34;)
    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) -&gt; 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&#34;^09\d{8}$&#34;)

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):
    &#34;&#34;&#34;使用者（含訂單歷史）&#34;&#34;&#34;
    orders: List[OrderResponse] = []
```

---

## ✅ 重點總結

### 巢狀模型

```python
class Inner(BaseModel):
    value: str

class Outer(BaseModel):
    inner: Inner              # 必填巢狀
    optional: Optional[Inner] = None  # 選填巢狀
    items: List[Inner] = []   # 巢狀列表
```

### 繼承模式

```python
# 基礎欄位
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)
```

### 泛型

```python
from typing import TypeVar, Generic

T = TypeVar(&#39;T&#39;)

class Wrapper(BaseModel, Generic[T]):
    data: T

# 使用
Wrapper[User](data=user)
```

---

## 🎤 面試這樣答

### Q: 為什麼要把 Create、Update、Response 分成不同的模型？

**答案：**

&gt; 因為不同操作需要不同的欄位：
&gt;
&gt; 1. **Create**：需要所有必填欄位，可能包含密碼
&gt; 2. **Update**：所有欄位都是選填，只更新提供的欄位
&gt; 3. **Response**：包含 ID、時間戳，但不包含敏感資料如密碼
&gt;
&gt; 分離模型可以：
&gt; - 更安全（不會不小心回傳密碼）
&gt; - 更清晰（每個操作的需求一目了然）
&gt; - 更好的文件（Swagger 會顯示正確的欄位）

---

## 🤓 小測驗

1. 如何讓巢狀模型變成選填？
   &lt;details&gt;
   &lt;summary&gt;點擊看答案&lt;/summary&gt;
   使用 Optional：`address: Optional[Address] = None`
   &lt;/details&gt;

2. `model_config = ConfigDict(from_attributes=True)` 的作用？
   &lt;details&gt;
   &lt;summary&gt;點擊看答案&lt;/summary&gt;
   允許從 ORM 物件（如 SQLAlchemy model）建立 Pydantic 模型
   &lt;/details&gt;

3. 如何建立可重用的分頁回應模型？
   &lt;details&gt;
   &lt;summary&gt;點擊看答案&lt;/summary&gt;
   使用泛型：`class PaginatedResponse(BaseModel, Generic[T])`
   &lt;/details&gt;

---

**上一篇：** [02-3. 自訂驗證器](./02-3)
**下一篇：** [02-5. 序列化與反序列化](./02-5)

---

最後更新：2025-12-17


---

> 作者: luk  
> URL: https://yoru-karu-blog-lalaluk-52581ac5e0cef170a3c8922c19182ecb6f7bd604.gitlab.io/posts/tutorial/fastapi/02-4/  

