You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

18 KiB

更大型的應用程式 - 多個檔案

如果你正在建置一個應用程式或 Web API,很少會把所有東西都放在單一檔案裡。

FastAPI 提供了一個方便的工具,讓你在維持彈性的同時,幫你組織應用程式的結構。

/// info | 資訊

如果你來自 Flask,這相當於 Flask 的 Blueprints。

///

範例檔案結構

假設你有如下的檔案結構:

.
├── app
│   ├── __init__.py
│   ├── main.py
│   ├── dependencies.py
│   └── routers
│   │   ├── __init__.py
│   │   ├── items.py
│   │   └── users.py
│   └── internal
│       ├── __init__.py
│       └── admin.py

/// tip | 提示

有好幾個 __init__.py 檔案:每個目錄或子目錄各一個。

這讓我們可以把一個檔案中的程式碼匯入到另一個檔案。

例如,在 app/main.py 你可以有一行:

from app.routers import items

///

  • app 目錄包含所有內容。它有一個空的 app/__init__.py 檔案,所以它是一個「Python 套件」(「Python 模組」的集合):app
  • 它包含一個 app/main.py 檔案。因為它在一個 Python 套件中(有 __init__.py 檔案的目錄),它是該套件的一個「模組」:app.main
  • 還有一個 app/dependencies.py 檔案,就像 app/main.py 一樣,它是一個「模組」:app.dependencies
  • 有一個子目錄 app/routers/,裡面有另一個 __init__.py 檔案,所以它是一個「Python 子套件」:app.routers
  • 檔案 app/routers/items.py 在一個套件 app/routers/ 內,因此它是一個子模組:app.routers.items
  • 同樣地,app/routers/users.py 是另一個子模組:app.routers.users
  • 還有一個子目錄 app/internal/,裡面有另一個 __init__.py 檔案,所以它又是一個「Python 子套件」:app.internal
  • 檔案 app/internal/admin.py 是另一個子模組:app.internal.admin

同樣的檔案結構,附上註解:

.
├── app                  # 「app」是一個 Python 套件
│   ├── __init__.py      # 這個檔案讓「app」成為「Python 套件」
│   ├── main.py          # 「main」模組,例如 import app.main
│   ├── dependencies.py  # 「dependencies」模組,例如 import app.dependencies
│   └── routers          # 「routers」是一個「Python 子套件」
│   │   ├── __init__.py  # 讓「routers」成為「Python 子套件」
│   │   ├── items.py     # 「items」子模組,例如 import app.routers.items
│   │   └── users.py     # 「users」子模組,例如 import app.routers.users
│   └── internal         # 「internal」是一個「Python 子套件」
│       ├── __init__.py  # 讓「internal」成為「Python 子套件」
│       └── admin.py     # 「admin」子模組,例如 import app.internal.admin

APIRouter

假設專門處理使用者的檔案是位於 /app/routers/users.py 的子模組。

你希望把與使用者相關的「路徑操作 (path operation)」從其他程式碼分離,讓結構更有條理。

但它仍然是同一個 FastAPI 應用程式 / Web API 的一部分(屬於同一個「Python 套件」)。

你可以使用 APIRouter 為該模組建立路徑操作。

匯入 APIRouter

你可以像對 FastAPI 類別那樣匯入並建立一個「實例」:

{* ../../docs_src/bigger_applications/app_an_py310/routers/users.py hl[1,3] title["app/routers/users.py"] *}

使用 APIRouter 宣告路徑操作

然後用它來宣告你的路徑操作。

用法就和 FastAPI 類別一樣:

{* ../../docs_src/bigger_applications/app_an_py310/routers/users.py hl[6,11,16] title["app/routers/users.py"] *}

你可以把 APIRouter 想成是「迷你版的 FastAPI」類別。

所有相同的選項都支援。

同樣的 parametersresponsesdependenciestags 等全都可用。

/// tip | 提示

在這個範例中,變數名叫 router,但你可以用任何你想用的名稱。

///

我們稍後會把這個 APIRouter 加進主要的 FastAPI 應用程式中,但先來看看相依性與另一個 APIRouter

相依性

我們發現應用程式的多個地方會用到一些相依性。

所以把它們放進獨立的 dependencies 模組(app/dependencies.py)。

接下來我們會用一個簡單的相依性來讀取自訂的 X-Token 標頭:

{* ../../docs_src/bigger_applications/app_an_py310/dependencies.py hl[3,6:8] title["app/dependencies.py"] *}

/// tip | 提示

為了簡化範例,我們使用了一個虛構的標頭。

但在真實情況下,使用內建的安全工具{.internal-link target=_blank}會有更好的效果。

///

另一個帶有 APIRouter 的模組

假設你還有一個模組 app/routers/items.py,專門處理應用程式中的「items」。

你有以下路徑操作:

  • /items/
  • /items/{item_id}

其結構與 app/routers/users.py 相同。

但我們想要更聰明地簡化一些程式碼。

我們知道這個模組中的所有路徑操作都有相同的:

  • 路徑 prefix/items
  • tags:(只有一個標籤:items
  • 額外的 responses
  • dependencies:它們都需要我們先前建立的 X-Token 相依性

因此,我們可以不必把這些都加在每個路徑操作上,而是把它們加在 APIRouter 上。

{* ../../docs_src/bigger_applications/app_an_py310/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *}

由於每個路徑操作的路徑都必須以 / 開頭,例如:

@router.get("/{item_id}")
async def read_item(item_id: str):
    ...

...所以 prefix 末尾不能帶有 /

因此,此處的 prefix 是 /items

我們也可以加上一個 tags 清單,以及會套用在此 router 內所有路徑操作上的額外 responses

我們還可以加上一個 dependencies 清單,這些相依性會加入此 router 內所有的路徑操作,並在對它們的每個請求上執行 / 解決。

/// tip | 提示

請注意,就像在路徑操作裝飾器中的相依性{.internal-link target=_blank}一樣,不會把任何值傳遞給你的路徑操作函式(path operation function)。

///

最後的結果是這些 item 的路徑如下:

  • /items/
  • /items/{item_id}

...正如我們預期的。

  • 它們會被標記為只有一個字串 "items" 的標籤清單。
    • 這些「標籤」對自動互動式文件系統(使用 OpenAPI)特別有用。
  • 它們都會包含預先定義的 responses
  • 這些路徑操作都會在執行前評估 / 執行其 dependencies 清單。
    • 如果你也在特定的路徑操作中宣告了相依性,這些相依性也會被執行。
    • Router 的相依性會先執行,然後是裝飾器中的 dependencies{.internal-link target=_blank},最後是一般參數相依性。
    • 你也可以加入帶有 scopesSecurity 相依性{.internal-link target=_blank}。

/// tip | 提示

APIRouter 中設定 dependencies,例如可以用來對一整組路徑操作要求驗證。即使沒有在每個路徑操作個別加入相依性也沒關係。

///

/// check | 檢查

prefixtagsresponsesdependencies 參數(就像許多其他情況一樣)只是 FastAPI 提供的功能,幫助你避免重複程式碼。

///

匯入相依性

這段程式碼在模組 app.routers.items(檔案 app/routers/items.py)中。

我們需要從模組 app.dependencies(檔案 app/dependencies.py)取得相依性函式。

因此我們用 .. 做相對匯入相依性:

{* ../../docs_src/bigger_applications/app_an_py310/routers/items.py hl[3] title["app/routers/items.py"] *}

相對匯入如何運作

/// tip | 提示

如果你對匯入的運作方式十分了解,可以直接跳到下一節。

///

單一的點號 .,如下:

from .dependencies import get_token_header

代表:

  • 從此模組(檔案 app/routers/items.py)所在的相同套件(目錄 app/routers/)開始...
  • 找到模組 dependencies(想像的檔案 app/routers/dependencies.py)...
  • 並從中匯入函式 get_token_header

但那個檔案不存在,我們的相依性在 app/dependencies.py

回想一下我們的應用 / 檔案結構長這樣:


兩個點號 ..,如下:

from ..dependencies import get_token_header

代表:

  • 從此模組(檔案 app/routers/items.py)所在的相同套件(目錄 app/routers/)開始...
  • 前往其父套件(目錄 app/)...
  • 然後在那裡找到模組 dependencies(檔案 app/dependencies.py)...
  • 並從中匯入函式 get_token_header

這就正確了!🎉


同樣地,如果我們用三個點號 ...,如下:

from ...dependencies import get_token_header

就代表:

  • 從此模組(檔案 app/routers/items.py)所在的相同套件(目錄 app/routers/)開始...
  • 前往其父套件(目錄 app/)...
  • 再前往那個套件的父層(沒有更上層的套件了,app 已是最上層 😱)...
  • 然後在那裡找到模組 dependencies(檔案 app/dependencies.py)...
  • 並從中匯入函式 get_token_header

那會指向 app/ 之上的某個套件,該套件需有自己的 __init__.py 等等。但我們沒有。所以在這個例子中會丟出錯誤。🚨

不過現在你知道它的運作方式了,因此無論你的應用有多複雜,你都可以使用相對匯入。🤓

加上一些自訂的 tagsresponsesdependencies

我們沒有把 /items 的 prefix 以及 tags=["items"] 加在每個路徑操作上,因為我們已經把它們加在 APIRouter 上了。

但我們仍可以在特定的路徑操作上再加上更多的 tags,以及一些只屬於該路徑操作的額外 responses

{* ../../docs_src/bigger_applications/app_an_py310/routers/items.py hl[30:31] title["app/routers/items.py"] *}

/// tip | 提示

這最後一個路徑操作會有組合後的標籤:["items", "custom"]

而且在文件中同時會有 404403 兩種回應。

///

主程式 FastAPI

現在,來看看 app/main.py 這個模組。

你會在這裡匯入並使用 FastAPI 類別。

這會是你的應用程式中把一切串起來的主檔案。

而隨著大多數的邏輯都放在各自的模組中,主檔案會相當簡潔。

匯入 FastAPI

照常匯入並建立 FastAPI 類別。

我們甚至可以宣告全域相依性{.internal-link target=_blank},它們會與各 APIRouter 的相依性合併:

{* ../../docs_src/bigger_applications/app_an_py310/main.py hl[1,3,7] title["app/main.py"] *}

匯入 APIRouter

現在我們匯入包含 APIRouter 的其他子模組:

{* ../../docs_src/bigger_applications/app_an_py310/main.py hl[4:5] title["app/main.py"] *}

由於 app/routers/users.pyapp/routers/items.py 是同一個 Python 套件 app 的子模組,我們可以用單一的點號 . 來進行「相對匯入」。

匯入如何運作

這段:

from .routers import items, users

代表:

  • 從此模組(檔案 app/main.py)所在的相同套件(目錄 app/)開始...
  • 尋找子套件 routers(目錄 app/routers/)...
  • 並從中匯入子模組 items(檔案 app/routers/items.py)與 users(檔案 app/routers/users.py)...

模組 items 會有一個變數 routeritems.router)。這就是我們在 app/routers/items.py 建立的那個 APIRouter 物件。

接著我們對 users 模組做一樣的事。

我們也可以這樣匯入:

from app.routers import items, users

/// info | 資訊

第一種是「相對匯入」:

from .routers import items, users

第二種是「絕對匯入」:

from app.routers import items, users

想了解更多關於 Python 套件與模組,請閱讀官方的模組說明文件

///

避免名稱衝突

我們直接匯入子模組 items,而不是只匯入它的變數 router

這是因為在子模組 users 中也有另一個名為 router 的變數。

如果我們像下面這樣一個接一個匯入:

from .routers.items import router
from .routers.users import router

來自 usersrouter 會覆蓋掉 itemsrouter,我們就無法同時使用兩者。

因此,為了能在同一個檔案中同時使用它們,我們直接匯入子模組:

{* ../../docs_src/bigger_applications/app_an_py310/main.py hl[5] title["app/main.py"] *}

usersitemsAPIRouter 納入

現在,把子模組 usersitemsrouter 納入:

{* ../../docs_src/bigger_applications/app_an_py310/main.py hl[10:11] title["app/main.py"] *}

/// info | 資訊

users.router 是位於 app/routers/users.py 檔案內的 APIRouter

items.router 是位於 app/routers/items.py 檔案內的 APIRouter

///

透過 app.include_router(),我們可以把每個 APIRouter 加到主要的 FastAPI 應用程式。

它會把該 router 的所有路由都納入成為應用的一部分。

/// note | 技術細節

實際上,它會在內部為 APIRouter 中宣告的每一個「路徑操作」建立一個對應的「路徑操作」。

所以在幕後,它實際運作起來就像是一個單一的應用。

///

/// check | 檢查

把 router 納入時不需要擔心效能。

這只會在啟動時花費微秒等級,且只發生一次。

因此不會影響效能。

///

以自訂的 prefixtagsresponsesdependencies 納入一個 APIRouter

現在,假設你的組織提供了一個 app/internal/admin.py 檔案給你。

它包含一個帶有一些管理員路徑操作的 APIRouter,並在組織內多個專案之間共用。

為了這個範例它會非常簡單。但假設因為它會與組織內的其他專案共用,我們不能直接修改它並把 prefixdependenciestags 等加在 APIRouter 上:

{* ../../docs_src/bigger_applications/app_an_py310/internal/admin.py hl[3] title["app/internal/admin.py"] *}

但當我們把這個 APIRouter 納入時,仍然希望設定自訂的 prefix,讓它所有的路徑操作都以 /admin 開頭;我們想用這個專案已經有的 dependencies 來保護它,並且要加入 tagsresponses

我們可以在不修改原始 APIRouter 的情況下,將這些參數傳給 app.include_router() 來達成:

{* ../../docs_src/bigger_applications/app_an_py310/main.py hl[14:17] title["app/main.py"] *}

如此一來,原始的 APIRouter 將保持不變,因此我們仍然可以把同一個 app/internal/admin.py 檔案與組織中的其他專案共用。

結果是在我們的應用中,來自 admin 模組的每個路徑操作都會有:

  • 前綴 /admin
  • 標籤 admin
  • 相依性 get_token_header
  • 回應 418 🍵

但這只會影響我們應用中的那個 APIRouter,不會影響任何其他使用它的程式碼。

例如,其他專案可以用不同的驗證方式搭配相同的 APIRouter

加上一個路徑操作

我們也可以直接把路徑操作加到 FastAPI 應用中。

這裡我們就加一下... 只是為了示範可以這麼做 🤷

{* ../../docs_src/bigger_applications/app_an_py310/main.py hl[21:23] title["app/main.py"] *}

而且它會和透過 app.include_router() 加入的其他路徑操作正確地一起運作。

/// info | 非常技術細節

注意:這是個非常技術性的細節,你大概可以直接略過。


APIRouter 不是被「掛載 (mount)」的,它們不會與應用的其他部分隔離開來。

這是因為我們要把它們的路徑操作包含進 OpenAPI 結構與使用者介面中。

由於無法將它們隔離並獨立「掛載」,所以這些路徑操作會被「複製」(重新建立),而不是直接包含進來。

///

檢查自動產生的 API 文件

現在,執行你的應用:

$ fastapi dev app/main.py

<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

然後開啟位於 http://127.0.0.1:8000/docs 的文件。

你會看到自動產生的 API 文件,包含來自所有子模組的路徑,使用正確的路徑(與前綴)與正確的標籤:

以不同的 prefix 多次納入同一個 router

你也可以用不同的前綴,對同一個 router 多次呼叫 .include_router()

例如,這對於在不同前綴下提供相同的 API 很有用,如 /api/v1/api/latest

這是進階用法,你可能不會需要,但若有需要它就在那裡。

在另一個 APIRouter 中納入一個 APIRouter

就像你可以在 FastAPI 應用中納入一個 APIRouter 一樣,你也可以在另一個 APIRouter 中納入一個 APIRouter,用法如下:

router.include_router(other_router)

請確保在把 router 納入 FastAPI 應用之前先這麼做,這樣 other_router 的路徑操作也會被包含進去。