跳到主要內容

Web Day 5 請求驗證與相依性注入

Web Day 5 請求驗證與相依性注入

執行需求:CPU 可跑。昨天的書本 API 雖然能跑,但只是「能驗證」而已。今天要把請求驗證做到生產等級:用 Pydantic v2 的自訂驗證器處理跨欄位約束、用 Annotated 與 Depends 把常用邏輯抽出來做成「相依性」,並用 yield 處理資料庫連線、token 驗證這類「用完要清掉」的資源。學完這篇,你會理解為什麼 FastAPI 工程師常說「Depends 是這個框架最被低估的功能」。

引言

寫過幾支 CRUD API 之後,你會發現很多邏輯是重複的:分頁參數(skip、limit)、排序參數(sort_by、order)、認證(從標頭拿 token、解碼、查使用者)、資料庫連線(開 connection、用完關掉)。如果每個端點都自己寫一份,很快就會變成「二十個端點各有自己的分頁寫法」這種維運噩夢。FastAPI 的相依性注入(Dependency Injection,DI)就是為了解決這個問題而設計的:把邏輯包成可重複使用的「相依性」,由框架自動注入到需要的端點。

同時,僅靠 Pydantic 的欄位層級驗證還不夠。實務上常見「兩個欄位要互相約束」的需求,例如「結束時間必須晚於開始時間」、「密碼與確認密碼要一致」、「建立訂單時數量必須大於零且不能超過庫存」。這類跨欄位邏輯必須寫在 Pydantic 模型裡(這樣驗證錯誤才會統一回 422),而不是在端點函式裡手寫。今天會帶你用 Pydantic v2 的 model_validator 處理這類情境。

這篇文章會做四件事:第一,介紹 FastAPI 的請求驗證全貌(路徑、查詢、標頭、Cookie、Body);第二,示範 Pydantic v2 的自訂驗證器;第三,深入相依性注入的設計哲學與 Depends 寫法;第四,建立一個「分頁 + 排序 + 過濾 + 共用驗證」的書本 API,把今天所有東西串起來。

FastAPI 的請求驗證全貌

FastAPI 把 HTTP 請求的不同部位對應到 Python 函式的參數,這個機制在 Day 4 已經介紹過。今天把整個家族一次攤開來看:

來源 注入方式 常用場景
路徑參數 item_id: int 資源 ID、必填
查詢參數 q: Annotated[str | None, Query(...)] = None 篩選、分頁、排序
標頭 x_token: Annotated[str, Header()] 認證 token、API key
Cookie session: Annotated[str, Cookie()] 瀏覽器 session
請求主體 payload: ItemCreate 建立、整批更新
表單 Form() 檔案上傳、傳統表單
檔案 File() 上傳檔案(Day 14)

所有這些注入都支援相同的驗證機制(Query、Header、Cookie、Body 都有 min_length、ge、pattern 等參數)。實務上建議統一用 Annotated[type, Validation()] 的寫法,不要把預設值與驗證擠在同一個參數裡,這樣規格書最清楚。

標頭驗證的小細節:HTTP 標頭的大小寫不敏感(X-Token 等於 x-token),但 FastAPI 預設會把 Python 函式參數名稱 x_token 自動轉成 x-token 來比對。如果你不想用這種轉換,可以用 Header(alias="X-API-Key") 明確指定。

Pydantic v2 自訂驗證器

Pydantic 提供兩種驗證器:欄位層級(field validator)與模型層級(model validator)。欄位層級驗證單一欄位的內容,模型層級驗證跨欄位的關係:

# Pydantic v2 的驗證器範例
from datetime import date

from pydantic import BaseModel, EmailStr, Field, field_validator, model_validator


class Reservation(BaseModel):
    """預約資料模型:跨欄位驗證(結束日期必須晚於開始日期)"""

    customer_name: str = Field(min_length=1, max_length=80)
    customer_email: EmailStr
    start_date: date
    end_date: date
    guests: int = Field(ge=1, le=20)

    @field_validator("customer_name")
    @classmethod
    def name_must_not_be_blank(cls, v: str) -> str:
        # 欄位層級:去除頭尾空白,空白字串視為無效
        v = v.strip()
        if not v:
            raise ValueError("姓名不可為空白")
        return v

    @model_validator(mode="after")
    def check_dates(self):
        # 模型層級:檢查 end_date 必須晚於 start_date
        if self.end_date <= self.start_date:
            raise ValueError("結束日期必須晚於開始日期")
        return self


# 合法
r = Reservation(
    customer_name="王小明",
    customer_email="wang@example.com",
    start_date=date(2025, 8, 1),
    end_date=date(2025, 8, 3),
    guests=2,
)
print(f"{r.customer_name}:{r.start_date} 到 {r.end_date},{r.guests} 位")
# 輸出:王小明:2025-08-01 到 2025-08-03,2 位

# 跨欄位錯誤
try:
    Reservation(
        customer_name="王小明",
        customer_email="wang@example.com",
        start_date=date(2025, 8, 3),
        end_date=date(2025, 8, 1),  # 早於 start_date
        guests=2,
    )
except Exception as e:
    print("驗證失敗:", type(e).__name__)
    # 輸出:驗證失敗: ValidationError

幾個關鍵寫法:@field_validator 必須加 @classmethod,這是 Pydantic v2 的新規範;@model_validator(mode="after") 在所有欄位驗證完成後執行,適合做跨欄位檢查;mode="before" 則會在欄位驗證之前執行,適合做「把字串轉成日期」這種預處理。實務上 mode="after" 是最常用的模式。

另一個常見需求是「資料清洗」。例如使用者輸入的電話號碼可能有空格、破折號、國際碼,可以用 field_validator 在驗證階段就清洗乾淨:

import re

from pydantic import BaseModel, Field


class OrderCreate(BaseModel):
    customer_name: str = Field(min_length=1, max_length=80)
    phone: str = Field(min_length=8, max_length=20)

    @field_validator("phone")
    @classmethod
    def normalize_phone(cls, v: str) -> str:
        # 去掉所有非數字字元;保留國際碼前綴(+)
        cleaned = re.sub(r"[^\d+]", "", v)
        if not cleaned:
            raise ValueError("電話號碼不可為空")
        return cleaned


# 輸入:手機格式不一
o = OrderCreate(customer_name="測試", phone="0912-345-678")
print(f"清洗後:{o.phone}")
# 輸出:清洗後:0912345678

把清洗邏輯寫在模型裡有兩個好處:第一,所有端點收到的資料都會被同樣清洗,避免某個端點忘了清洗導致資料不一致;第二,清洗後的資料會反映在 model_dump() 的輸出,方便日誌與除錯。

相依性注入:把常用邏輯抽成可重用元件

相依性注入(Dependency Injection,DI)聽起來很抽象,其實概念很簡單:把「端點函式需要用到的資源或邏輯」抽出來,讓 FastAPI 在呼叫端點之前先幫你準備好。準備的方式是定義一個函式(稱為相依性),用 Depends() 註記在端點參數上。

先看一個簡單的例子:

# 相依性注入的最小範例
from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


def common_params(
    q: Annotated[str | None, "查詢字串"] = None,
    skip: Annotated[int, "略過筆數"] = 0,
    limit: Annotated[int, "取幾筆"] = 10,
):
    # 這是「相依性函式」:回傳一個 dict
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items")
def list_items(params: Annotated[dict, Depends(common_params)]):
    # Depends 會自動呼叫 common_params 並把結果注入這裡
    return params


# 呼叫:GET /items?q=hello&skip=0&limit=10
# FastAPI 會自動呼叫 common_params(q="hello", skip=0, limit=10)
# 把結果 {"q": "hello", "skip": 0, "limit": 10} 注入 list_items

這個例子看起來簡單到沒什麼,但「把所有端點都要用的參數」抽成相依性,正是大型後端專案的基礎。實務上你可以把分頁、排序、過濾、共用驗證、認證、資料庫連線都做成相依性,讓端點函式專注在「拿到資料後要做什麼」。

FastAPI 0.95+ 推薦用 Annotated[type, Depends(fn)] 寫相依性,比 name: type = Depends(fn) 更清楚型別意圖。本系列從這篇開始所有相依性都用 Annotated 寫法。

用 yield 處理資源生命週期

某些資源「用完要清掉」:資料庫連線(要 close)、檔案(要關閉)、交易(要 commit 或 rollback)、token 驗證(要記錄)。FastAPI 的相依性支援 yield 語法,把「建立」與「清理」寫在同一個函式裡:

# 用 yield 寫資料庫連線相依性(Day 6 會接 SQLite,這裡先用假連線示範語法)
from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


class FakeDB:
    """假資料庫連線:只是為了示範 yield 的生命週期"""

    def __init__(self):
        print("開啟連線")  # 模擬開啟資源

    def query(self, sql: str) -> list[dict]:
        return [{"id": 1, "name": "fake"}]

    def close(self):
        print("關閉連線")  # 模擬釋放資源


def get_db():
    # yield 之前的程式碼在端點「之前」執行
    db = FakeDB()
    try:
        yield db  # 把資源交給端點
    finally:
        # yield 之後的程式碼在端點「之後」執行(即使端點出錯也會跑)
        db.close()


@app.get("/items")
def list_items(db: Annotated[FakeDB, Depends(get_db)]):
    return db.query("SELECT * FROM items")


# 呼叫:GET /items
# 印出順序:開啟連線 -> ... -> 關閉連線

關鍵設計:yield 把資源交給端點,端點函式執行完(不論成功或失敗)後,框架會自動執行 yield 後面的清理區塊。這保證資源一定會被釋放,避免連線洩漏(connection leak)。如果你的端點函式拋出例外,finally 還是會跑——這對資料庫交易特別重要(先 rollback 再 close)。

另一個常見的 yield 應用是「依賴鏈(dependency chain)」:一個相依性可以呼叫另一個相依性,FastAPI 會依照呼叫順序建立、反向拆除:

# 依賴鏈範例
from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


def query_token(token: Annotated[str | None, "查詢參數 token"] = None):
    return token or "anonymous"


def require_user(token: Annotated[str, Depends(query_token)]):
    # 這個相依性依賴另一個
    if token == "anonymous":
        raise ValueError("需要登入")
    return {"user": f"user-{token}"}


@app.get("/profile")
def profile(user: Annotated[dict, Depends(require_user)]):
    return user

FastAPI 在處理 /profile 時會自動先呼叫 query_token,把結果注入 require_user,再把 require_user 的結果注入 profile。這個鏈可以無限延伸,Day 12 介紹 JWT 認證時你會看到一個完整的應用範例。

完整實作:分頁 + 排序 + 過濾 + 共用驗證

把今天所有東西一次做完。我們擴充昨天的書本 API,加入:

  • 分頁與排序做成共用相依性
  • 作者與年份過濾
  • 書本建立時的跨欄位驗證(年份範圍)
  • 用 yield 模擬資料庫連線

建立 src/book_api/main.py:

# src/book_api/main.py
# 書本管理 API(含分頁、排序、過濾、相依性注入)
from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, Query
from pydantic import BaseModel, ConfigDict, Field, model_validator

app = FastAPI(
    title="書本管理 API",
    description="Day 5:分頁、排序、過濾、相依性注入",
    version="0.2.0",
)


# ---------- 資料模型 ----------
class BookBase(BaseModel):
    title: str = Field(min_length=1, max_length=200)
    author: str = Field(min_length=1, max_length=80)
    year: int = Field(ge=0, le=2100)


class BookCreate(BookBase):
    @model_validator(mode="after")
    def check_year_reasonable(self):
        # 跨欄位:年份不能超過「現在 + 1」(不允許預言書)
        import datetime
        next_year = datetime.date.today().year + 1
        if self.year > next_year:
            raise ValueError(f"年份不可超過 {next_year}")
        return self


class Book(BookBase):
    id: int
    model_config = ConfigDict(from_attributes=True)


# ---------- 共用相依性 ----------
class Pagination:
    """分頁參數的容器"""

    def __init__(
        self,
        skip: Annotated[int, Query(ge=0, description="略過筆數")] = 0,
        limit: Annotated[int, Query(ge=1, le=100, description="取幾筆")] = 20,
    ):
        self.skip = skip
        self.limit = limit


class Sorting:
    """排序參數的容器"""

    def __init__(
        self,
        sort_by: Annotated[str, Query(pattern="^(id|title|author|year)$")] = "id",
        order: Annotated[str, Query(pattern="^(asc|desc)$")] = "asc",
    ):
        self.sort_by = sort_by
        self.order = order


# 給 endpoint 用的型別別名
PaginationDep = Annotated[Pagination, Depends()]
SortingDep = Annotated[Sorting, Depends()]


# ---------- 假資料庫(用 yield 模擬連線) ----------
class FakeDB:
    def __init__(self):
        self._books: dict[int, Book] = {
            1: Book(id=1, title="Python 程式設計實戰", author="Hao", year=2024),
            2: Book(id=2, title="FastAPI 入門", author="Wei", year=2025),
            3: Book(id=3, title="資料庫設計", author="Hao", year=2023),
            4: Book(id=4, title="Clean Code", author="Robert", year=2008),
        }

    def list(self) -> list[Book]:
        return list(self._books.values())

    def get(self, book_id: int) -> Book | None:
        return self._books.get(book_id)

    def add(self, book: Book) -> Book:
        self._books[book.id] = book
        return book

    def close(self):
        pass


def get_db():
    db = FakeDB()
    try:
        yield db
    finally:
        db.close()


DBDep = Annotated[FakeDB, Depends(get_db)]


# ---------- 端點 ----------
@app.get("/books", response_model=list[Book], summary="列出書本")
def list_books(
    db: DBDep,
    pagination: PaginationDep,
    sorting: SortingDep,
    author: Annotated[str | None, Query(description="依作者過濾")] = None,
    year: Annotated[int | None, Query(ge=0, le=2100)] = None,
):
    items = db.list()
    if author is not None:
        items = [b for b in items if b.author == author]
    if year is not None:
        items = [b for b in items if b.year == year]
    items.sort(key=lambda b: getattr(b, sorting.sort_by), reverse=(sorting.order == "desc"))
    return items[pagination.skip : pagination.skip + pagination.limit]


@app.get("/books/{book_id}", response_model=Book)
def get_book(db: DBDep, book_id: int):
    book = db.get(book_id)
    if book is None:
        raise HTTPException(status_code=404, detail="book not found")
    return book


@app.post("/books", response_model=Book, status_code=201)
def create_book(db: DBDep, payload: BookCreate):
    next_id = max(db._books.keys(), default=0) + 1
    new = Book(id=next_id, **payload.model_dump())
    return db.add(new)


# 啟動:uvicorn book_api.main:app --reload --port 8000

這個版本做了幾個重要的設計選擇:

  • Pagination 與 Sorting 用 class 封裝參數,端點函式拿到的是物件,可以直接存取 pagination.skip。
  • 用型別別名 PaginationDep、SortingDep、DBDep 簡化簽章,避免每個端點都寫 Annotated[X, Depends()]。
  • BookCreate 的 model_validator 在年份超過「明年」時拒絕(避免有人上傳「2026 年的書」這種測試資料)。
  • get_db 用 yield 寫連線生命週期,端點用完自動呼叫 close()。

用 httpx 測試分頁與排序:

# tests/test_pagination.py
import httpx

BASE = "http://127.0.0.1:8000"


def main():
    with httpx.Client(base_url=BASE, timeout=5.0) as client:
        # 1. 分頁:取第 2 頁(skip=2, limit=2)
        r = client.get("/books", params={"skip": 2, "limit": 2})
        print("GET /books?skip=2&limit=2 ->", r.status_code, r.json())
        # 輸出(範例):[{...id 3}, {...id 4}]

        # 2. 排序:依年份降序
        r = client.get("/books", params={"sort_by": "year", "order": "desc"})
        print("GET /books?sort_by=year&order=desc ->", r.status_code, [b["year"] for b in r.json()])
        # 輸出(範例):[2025, 2024, 2023, 2008]

        # 3. 過濾:作者是 Hao
        r = client.get("/books", params={"author": "Hao"})
        print("GET /books?author=Hao ->", r.status_code, r.json())
        # 輸出(節錄):[{...id 1, author 'Hao'}, {...id 3, author 'Hao'}]

        # 4. 錯誤:排序欄位非法
        r = client.get("/books", params={"sort_by": "evil"})
        print("GET /books?sort_by=evil ->", r.status_code, r.json())
        # 輸出(節錄):422 - pattern 不符合

        # 5. 錯誤:建立超未來的書
        r = client.post(
            "/books",
            json={"title": "未來的書", "author": "Hao", "year": 9999},
        )
        print("POST /books (year=9999) ->", r.status_code, r.json())
        # 輸出(節錄):422 - 年份範圍錯誤


main()

這個迴圈把分頁、排序、過濾、錯誤驗證一次展示。實務上你會把這些呼叫包成 pytest 測試(Day 16 會示範 TestClient)。今天重點在於「驗證邏輯寫對地方」——所有 422 都來自 Pydantic 或 FastAPI 的自動驗證,沒有任何一行手寫的 if 判斷。

常見錯誤與踩雷

第一個常見的踩雷是「忘了寫 @classmethod」。Pydantic v2 的 field_validator 必須搭配 @classmethod,否則會在執行時報錯(TypeError)。這是從 Pydantic v1 升級上來的人最常見的錯誤,看到驗證器沒生效就檢查這裡。

第二個是「Depends 用 class 而不用 function」。FastAPI 允許 Depends(SomeClass),這會把 SomeClass() 當作相依性。這對「共享狀態」很方便,但要注意:每個請求都會建立新的 instance(FastAPI 的相依性預設是 per-request),如果你的 class 裡有昂貴的初始化,要改成模組層級的 singleton(Day 22 會討論快取)。

第三個是「yield 後的例外被吞掉」。如果在 yield 之後的清理區塊(finally 內)發生例外,那個例外會覆蓋原本的端點例外,導致除錯困難。實務上 close() 等清理動作應該設計成「不會失敗」,必要時把例外記錄到日誌就好,不要讓它傳出去。

效能與實務提醒

相依性注入的代價:每個請求都會額外呼叫相依性函式。在「Pagination()」這種輕量物件上成本幾乎為零;但如果相依性要做資料庫連線、外部 API 呼叫、讀取設定檔,就要考慮共用。FastAPI 提供 Depends(get_settings, use_cache=True) 機制(0.117 之後更明確),可以把結果快取在同一次請求裡;模組層層級的 singleton(直接在模組裡 settings = load_settings())也是常見做法。

Pydantic 驗證效能很好(v2 用 Rust 核心),但巢狀過深的模型(例如 5 層巢狀)在大量請求下會成為瓶頸。如果你發現 API 在驗證階段花太多時間,可以:用 model_construct() 跳過驗證(只在信任內部資料時用)、把大型模型拆成幾個小模型、或考慮 Pydantic 的 TypeAdapter 做更精細的控制。本系列規模不大,通常不會碰到這個瓶頸。

排序與過濾目前是在 Python 裡做的(db.list() 後用 list comprehension 過濾)。Day 6 換成 SQLite 之後,這些動作會下沉到資料庫層,可以用 WHERE 與 ORDER BY 子句做,效能會比記憶體內過濾好很多。今天先用記憶體版專注在「驗證與 DI 的觀念」,效能留給後面。

小結

今天把 FastAPI 的兩個進階主題一次講完:請求驗證的進階用法(Pydantic v2 的 field_validator、model_validator、自訂清洗邏輯)與相依性注入(Depends、Annotated、yield 處理資源生命週期、依賴鏈)。我們用一個擴充版的書本 API 把這些東西組起來:分頁與排序抽成共用相依性、建立時的跨欄位驗證、yield 模擬資料庫連線。這套結構在後續每天的範例都會沿用,是整個系列最常被用到的子集合之一。

進階主題:全域相依性與例外處理

前面介紹的 Depends 是「端點等級」的相依性,但有些邏輯是「全應用等級」的:例如「每個請求都要記錄開始時間以便計算耗時」、「每個請求都要驗證 API key」、「每個請求發生例外都要回傳統一格式」。這時用 FastAPI 的「全域相依性」與「例外處理器」就對了。

# global_deps.py
# 全域相依性 + 例外處理器範例
from datetime import datetime, timezone
from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, Request

app = FastAPI(title="全域相依性示範")


# 全域相依性:計算每個請求的耗時
async def log_request_time(request: Request):
    request.state.start_time = datetime.now(timezone.utc)


# 全域相依性:簡單的 API key 驗證
async def verify_api_key(request: Request):
    api_key = request.headers.get("X-API-Key")
    if api_key != "demo-key":
        raise HTTPException(status_code=401, detail="invalid API key")


# 把全域相依性掛到 app 上
app.include_router(
    # 為了簡化,直接在 FastAPI 實例上掛 middleware
    # 實際專案會用 app.middleware("http") 或 router
)


# 例外處理器:把所有 HTTPException 統一格式
from fastapi.responses import JSONResponse


@app.exception_handler(HTTPException)
async def custom_http_exception_handler(request: Request, exc: HTTPException):
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "error": {
                "code": exc.status_code,
                "message": exc.detail,
                "path": str(request.url.path),
            }
        },
    )


@app.get("/hello")
async def hello():
    return {"message": "Hello, FastAPI!"}

這段範例展示三個進階機制。第一,request.state.start_time 是 FastAPI 提供的「每個請求私有空間」,可以在全域相依性裡塞值,在端點函式裡讀出來——這對「計算耗時」、「記錄使用者資訊」非常有用。第二,@app.exception_handler 讓你把某一類例外統一包裝成想要的回應格式,這對「錯誤回應結構」的一致性非常關鍵。第三個 verify_api_key 雖然簡單,但展示了「全域驗證」的設計——後續 Day 12 介紹 JWT 認證時,會用同樣的機制檢查 token。

第二個進階主題:用「依賴覆寫(dependency override)」做測試時的取代。這個機制讓你在測試時把某個相依性換成假的,例如把「連到正式資料庫」換成「連到測試資料庫」、把「送 email」換成「記錄到記憶體」。

# test_overrides.py
# 用 FastAPI 的 dependency_overrides 做測試時的取代
import os
from typing import Generator

import httpx
from fastapi.testclient import TestClient

from main import app, get_session


# 1. 寫一個假的 get_session
def fake_get_session() -> Generator[dict, None, None]:
    """測試用的假 session:直接回傳假資料"""
    yield {
        "items": [
            {"id": 1, "title": "測試書", "author": "Test", "year": 2024}
        ]
    }


# 2. 註冊 override
app.dependency_overrides[get_session] = fake_get_session


# 3. 用 TestClient 測試
client = TestClient(app)
r = client.get("/books")
print(r.status_code, r.json())
# 輸出(範例):200 [{'id': 1, 'title': '測試書', 'author': 'Test', 'year': 2024}]

dependency_overrides 是 FastAPI 對「測試替身(test double)」的內建支援。它的好處是你不用為了測試去改任何端點函式,只要在測試開頭 app.dependency_overrides[xxx] = fake_xxx 就能把相依性換成假貨。這在 Day 16、Day 17 介紹 pytest 與 fixture 時會大量使用,今天先記得這個機制的存在。

第三個進階主題:用 Depends(use_cache=False) 強制每次呼叫。預設情況下,同一個端點的同一個相依性只會被執行一次(結果會被重用)。但在某些情境你需要「每次都重新執行」,例如「每個請求都讀取最新的設定檔」:

# non_cached_dep.py
# 強制每次都重新執行的相依性
from typing import Annotated
import os
import time

from fastapi import Depends, FastAPI

app = FastAPI()


def get_config(force_reload: Annotated[bool, "強制重新讀取"] = False):
    """讀取設定檔;force_reload=True 時重新讀一次"""
    if force_reload:
        time.sleep(0.1)  # 模擬讀檔耗時
    return {"loaded_at": time.time(), "env": os.getenv("ENV", "dev")}


# 一般呼叫:會被快取(同一請求內只執行一次)
@app.get("/normal")
def normal(config: Annotated[dict, Depends(get_config)]):
    return config


# 強制重新讀取:每次都執行
@app.get("/fresh")
def fresh(config: Annotated[dict, Depends(get_config, use_cache=False)]):
    # FastAPI 0.116+ 支援 use_cache=False
    return config

這個 use_cache=False 在 FastAPI 0.115+ 開始正式支援(本系列固定在 0.116,完整支援)。它的語意是「這個相依性在同一次請求內不要重用快取,每次呼叫都重新執行」。這個設計在「讀取設定檔、檢查時間戳記、生成 UUID」這類「不該被快取」的情境特別有用。

到這裡我們已經把請求驗證與相依性注入的所有進階主題走完。今天學到的東西涵蓋了:欄位驗證、跨欄位驗證、自訂清洗、共用相依性、yield 資源管理、依賴鏈、全域相依性、例外處理、測試覆寫、use_cache 控制。這是 FastAPI 框架裡最精華的部分之一,後續的認證、測試、資料庫章節都會建立在這個基礎上。

結語

今天把驗證與 DI 都講完了,但資料還存在記憶體裡。明天,我們要進入「資料持久化」的主題:SQLModel 與 SQLite。你會學到 ORM 的核心觀念、用 SQLModel 定義資料模型、用 SQLite 做本機儲存、以及怎麼把今天的 FakeDB 換成真正的資料庫。一旦資料可以持久化,整個 API 才真的開始像「系統」,而不只是「腳本」。

延伸資源

  • FastAPI 相依性教學(2025):https://fastapi.tiangolo.com/tutorial/dependencies/
  • Pydantic v2 Validators(2025):https://docs.pydantic.dev/latest/concepts/validators/
  • PEP 593 - Annotated(2019):https://peps.python.org/pep-0593/
  • FastAPI 進階 DI:全域相依性(2025):https://fastapi.tiangolo.com/tutorial/dependencies/global-dependencies/
  • Real Python:FastAPI Dependencies(2025):https://realpython.com/fastapi-dependencies-injection/
  • Python Type Hints(PEP 484,2014):https://peps.python.org/pep-0484/

留言

這個網誌中的熱門文章

Day 2 變數與資料型別

Day 2 變數與資料型別 引言 寫程式的過程中,變數與資料型別是處理資料的基礎。變數是存放資料的容器,資料型別則決定這筆資料有哪些特性、可以進行哪些操作。學會定義變數、認識各種資料型別,是學好 Python 的關鍵一步。 這篇文章會帶你了解 Python 中變數的觀念、如何定義變數,以及常見的資料型別,包括整數、浮點數、字串、布林值,還有串列、元組、字典與集合等容器型別。我們也會介紹變數的命名規則與撰寫風格建議,以及如何用 type() 檢查資料型別。 什麼是變數?如何在 Python 中定義變數 變數是在程式執行時用來存放資料的名稱。透過定義變數,我們可以給一筆資料一個名字,並在程式的其他地方用這個名字取用該筆資料。在 Python 中,變數不需要事先宣告型別,因為 Python 是動態型別語言,變數的型別由指定給它的值決定。 定義變數的基本語法 在 Python 中定義變數非常簡單,只要用賦值符號 = 把值指定給變數即可。例如: x = 5 # 定義變數 x,並把整數 5 賦值給它 name = "Alice" # 定義變數 name,並把字串 "Alice" 賦值給它 在這裡,x 是一個變數,被賦予整數 5;name 是另一個變數,被賦予字串 "Alice"。 變數的更新與覆寫 變數的值可以修改,也就是說,我們可以在程式的不同地方給同一個變數新的值。例如: x = 10 # x 最初被賦予 10 x = 15 # x 的值現在被更新為 15 這樣就能依照需求,在程式執行過程中靈活調整變數的值。 Python 的動態型別系統 Python 和某些靜態型別語言不同,定義變數時不需要宣告型別。賦值時,Python 會根據值自動判斷變數的型別。例如: x = 5 # x 是整數 x = 3.14 # x 變成浮點數 x = "Hi" # x 變成字串 同一個變數在程式執行過程中可以存放不同型別的值,這是 Python 的彈性之一。 常見資料型別 在 Python 中,資料型別決定我們可以對變數進行哪些操作...

Day 1 Python 簡介與環境設定

Day 1 Python 簡介與環境設定 引言 在現在的科技環境裡,程式設計已經是一項重要技能。無論你是對資料科學有興趣、想成為開發者,或是想踏入人工智慧(AI)領域,學會寫程式都能明顯提升你的競爭力。在眾多程式語言中,Python 因為語法簡單、功能強大、應用範圍廣泛,成為許多人進入程式世界的第一選擇。這篇文章會帶你認識 Python 的背景與優勢,並一步步教你在不同系統上安裝與設定 Python 開發環境,最後寫出第一支 Python 程式。 為什麼選擇 Python? Python 是一種高階程式語言,由 Guido van Rossum 在 1991 年發布。Python 的設計哲學強調程式碼的可讀性,並用縮排來定義程式區塊,這點和許多使用大括號的語言不同。簡潔的語法讓它成為初學者的理想選擇;就算是經驗豐富的開發者,也能用它完成複雜的專案。 Python 的優勢如下: 簡單易學 :Python 的語法清楚、結構簡潔,初學者很快就能上手。和其他語言相比,學習曲線相對平緩,不需要先弄懂一堆複雜觀念,就能開始寫程式。 應用範圍廣泛 :從資料科學、網頁開發、人工智慧、機器學習、自動化測試到網路爬蟲,Python 都有大量開源函式庫與工具支援,而且在這些領域都扮演關鍵角色。 豐富的函式庫與框架 :Python 的函式庫生態系非常龐大。做資料分析有 NumPy、Pandas;開發網站有 Django、Flask;做深度學習有 TensorFlow、PyTorch。各種需求幾乎都能找到對應的套件,讓開發更有效率。 跨平台支援 :Python 支援 Windows、macOS、Linux 等作業系統,程式通常不需要太多修改就能跨平台執行,讓開發與部署更有彈性。 活躍的社群 :Python 擁有龐大的開發者社群。學習或開發上遇到問題,幾乎都能在社群與論壇(例如 Stack Overflow)找到答案,對初學者來說是很強的後盾,也能減少卡關時的挫折感。 Python 的應用領域 Python 的流行與強大功能,讓許多領域都開始大量使用它。以下是幾個常見的應用方向: 資料科學 :隨著大數據與人工智慧興起,資料科學大量使用 Python。NumPy、Pandas 與 Matplotlib 等工具能處理和分析龐...

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門 隨著人工智慧(AI)與深度學習(Deep Learning)快速發展,越來越多人對這些技術產生興趣。不論你是想踏入 AI 領域的初學者,還是已經有程式基礎的開發者,學好 Python 與深度學習框架(例如 PyTorch),都能為你打開更多可能。 為什麼選擇 Python? Python 已經是資料科學與人工智慧領域的首選語言。它的語法簡潔、容易上手,而且擁有龐大的生態系與大量開源函式庫。無論是資料處理、資料視覺化,還是建立機器學習與深度學習模型,Python 都能勝任。對想進入 AI 或資料科學領域的人來說,它幾乎是必備工具。 PyTorch 是什麼? PyTorch 是由 Meta(原 Facebook)AI 研究團隊開發的開源深度學習框架,以易用、靈活和動態計算圖著稱,是許多 AI 研究人員與開發者的首選。相較於其他框架,PyTorch 的寫法更貼近原生 Python,對初學者相對友善。無論是簡單的實驗,還是複雜的深度學習模型,PyTorch 都能提供強大的支援。 這個系列能帶給你什麼? 這個系列會從 Python 的基礎開始,帶你一步一步學習,最後能自己用 PyTorch 建立深度學習模型。即使你完全沒有寫過程式,也能跟著文章的節奏累積技能,理解 AI 與深度學習的核心觀念。 本系列涵蓋的主題 Python 基礎:從變數、條件判斷到函式與模組。 資料處理工具:用 NumPy 與 Pandas 有效率地操作資料。 資料視覺化:用 Matplotlib 與 Seaborn 把資料畫成圖表。 深度學習的數學基礎:線性代數、微積分與機率。 PyTorch 入門:理解張量、模型建構與 GPU 加速。 基礎深度學習模型:CNN 與 RNN 的實作應用。 深度學習專案實戰:從資料前處理到模型部署的端到端流程。 誰適合這個系列? 程式初學者 :如果你對 AI 充滿好奇,卻還沒寫過程式,系列的第一部分會帶你快速上手 Python,並幫助你理解深度學習的基本觀念。 資料科學愛好者 :如果你已經熟悉一些資料處理方法,進階部分會教你如何用 PyTorch 建構深度學習模型。 開發者與研究人員 :想更深入了...