Skip to content

設定與環境變數

🌐 AI 與人類共同完成的翻譯

此翻譯由人類指導的 AI 完成。🤝

可能會有對原意的誤解,或讀起來不自然等問題。🤖

你可以透過協助我們更好地引導 AI LLM來改進此翻譯。

英文版

在許多情況下,你的應用程式可能需要一些外部設定或組態,例如密鑰、資料庫憑證、電子郵件服務的憑證等。

這些設定大多是可變的(可能會改變),像是資料庫 URL。也有許多可能是敏感資訊,例如密鑰。

因此,通常會透過環境變數提供這些設定,讓應用程式去讀取。

Tip

若想了解環境變數,你可以閱讀環境變數

型別與驗證

這些環境變數只能處理文字字串,因為它們在 Python 之外,必須與其他程式與系統的其餘部分相容(甚至跨作業系統,如 Linux、Windows、macOS)。

這表示在 Python 中自環境變數讀取到的任何值都會是 str,而任何轉型成其他型別或驗證都必須在程式碼中完成。

Pydantic Settings

幸好,Pydantic 提供了很好的工具,可用來處理由環境變數而來的設定:Pydantic:設定管理

安裝 pydantic-settings

首先,請先建立你的虛擬環境,啟用它,然後安裝 pydantic-settings 套件:

$ pip install pydantic-settings
---> 100%

當你用 all extras 安裝時,它也會一併包含在內:

$ pip install "fastapi[all]"
---> 100%

建立 Settings 物件

從 Pydantic 匯入 BaseSettings 並建立子類別,與建立 Pydantic model 的方式非常類似。

就像使用 Pydantic model 一樣,你用型別註解宣告類別屬性,並可選擇性地提供預設值。

你可以使用與 Pydantic model 相同的所有驗證功能與工具,例如不同的資料型別與透過 Field() 進行額外驗證。

from fastapi import FastAPI
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50


settings = Settings()
app = FastAPI()


@app.get("/info")
async def info():
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Tip

如果你想要可以直接複製貼上的範例,先別用這個,請改用本文最後一個範例。

接著,當你建立該 Settings 類別的實例(此處為 settings 物件)時,Pydantic 會以不區分大小寫的方式讀取環境變數,因此,即使環境變數是大寫的 APP_NAME,也會被讀入屬性 app_name

然後它會轉換並驗證資料。因此,當你使用該 settings 物件時,你會得到你宣告的型別的資料(例如 items_per_user 會是 int)。

使用 settings

接著你可以在應用程式中使用新的 settings 物件:

from fastapi import FastAPI
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50


settings = Settings()
app = FastAPI()


@app.get("/info")
async def info():
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

執行伺服器

接下來,你可以在啟動伺服器時,將設定以環境變數傳入。舉例來說,你可以設定 ADMIN_EMAILAPP_NAME

$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.py

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

Tip

要為單一指令設定多個環境變數,只要用空白分隔它們,並全部放在指令前面即可。

如此一來,admin_email 設定會被設為 "deadpool@example.com"

app_name 會是 "ChimichangApp"

items_per_user 則會保留其預設值 50

在另一個模組中的設定

你也可以把這些設定放在另一個模組檔案中,就像在更大的應用程式 - 多個檔案所示。

例如,你可以有一個 config.py 檔案如下:

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50


settings = Settings()

然後在 main.py 檔案中使用它:

from fastapi import FastAPI

from .config import settings

app = FastAPI()


@app.get("/info")
async def info():
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Tip

你也需要一個 __init__.py 檔案,詳見更大的應用程式 - 多個檔案

在相依中的設定

在某些情境中,從相依(dependency)提供設定,會比在各處使用一個全域的 settings 物件更有用。

這在測試時特別實用,因為你可以很容易用自訂的設定來覆寫一個相依。

設定檔

延續前一個範例,你的 config.py 可以像這樣:

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

注意現在我們不再建立預設實例 settings = Settings()

主應用程式檔案

現在我們建立一個相依,回傳新的 config.Settings()

from functools import lru_cache
from typing import Annotated

from fastapi import Depends, FastAPI

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Annotated[Settings, Depends(get_settings)]):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from functools import lru_cache

from fastapi import Depends, FastAPI

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Settings = Depends(get_settings)):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Tip

我們稍後會討論 @lru_cache

現在你可以先把 get_settings() 視為一般函式。

接著我們可以在路徑操作函式 (path operation function) 中將它宣告為相依,並在需要的地方使用它。

from functools import lru_cache
from typing import Annotated

from fastapi import Depends, FastAPI

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Annotated[Settings, Depends(get_settings)]):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from functools import lru_cache

from fastapi import Depends, FastAPI

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Settings = Depends(get_settings)):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

設定與測試

接著,在測試時要提供不同的設定物件會非常容易,只要為 get_settings 建立相依覆寫(dependency override)即可:

from fastapi.testclient import TestClient

from .config import Settings
from .main import app, get_settings

client = TestClient(app)


def get_settings_override():
    return Settings(admin_email="testing_admin@example.com")


app.dependency_overrides[get_settings] = get_settings_override


def test_app():
    response = client.get("/info")
    data = response.json()
    assert data == {
        "app_name": "Awesome API",
        "admin_email": "testing_admin@example.com",
        "items_per_user": 50,
    }
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from fastapi.testclient import TestClient

from .config import Settings
from .main import app, get_settings

client = TestClient(app)


def get_settings_override():
    return Settings(admin_email="testing_admin@example.com")


app.dependency_overrides[get_settings] = get_settings_override


def test_app():
    response = client.get("/info")
    data = response.json()
    assert data == {
        "app_name": "Awesome API",
        "admin_email": "testing_admin@example.com",
        "items_per_user": 50,
    }

在相依覆寫中,我們在建立新的 Settings 物件時設定 admin_email 的新值,然後回傳該新物件。

接著我們就可以測試它是否被使用。

讀取 .env

如果你有許多設定,而且在不同環境中可能常常變動,將它們放在一個檔案中,然後像讀取環境變數一樣自該檔案讀取,可能會很實用。

這種作法很常見,這些環境變數通常放在 .env 檔中,而該檔案被稱為「dotenv」。

Tip

在類 Unix 系統(如 Linux 與 macOS)中,以點(.)開頭的檔案是隱藏檔。

但 dotenv 檔並不一定必須使用這個確切的檔名。

Pydantic 透過外部函式庫支援讀取這類型的檔案。你可以閱讀更多:Pydantic Settings:Dotenv (.env) 支援

Tip

要讓這個功能運作,你需要 pip install python-dotenv

.env

你可以有一個 .env 檔如下:

ADMIN_EMAIL="deadpool@example.com"
APP_NAME="ChimichangApp"

.env 讀取設定

然後更新你的 config.py

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

    model_config = SettingsConfigDict(env_file=".env")
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

    model_config = SettingsConfigDict(env_file=".env")

Tip

model_config 屬性僅用於 Pydantic 的設定。你可以閱讀更多:Pydantic:概念:設定

在這裡我們在 Pydantic 的 Settings 類別中定義設定 env_file,並將其值設為要使用的 dotenv 檔名。

使用 lru_cache 只建立一次 Settings

從磁碟讀取檔案通常是昂貴(慢)的操作,所以你可能希望只做一次,然後重複使用同一個設定物件,而不是在每個請求都讀取。

但每次我們這樣做:

Settings()

都會建立一個新的 Settings 物件,而且在建立時會再次讀取 .env 檔。

如果相依函式只是像這樣:

def get_settings():
    return Settings()

我們就會為每個請求建立該物件,並在每個請求都讀取 .env 檔。⚠️

但由於我們在上方使用了 @lru_cache 裝飾器,Settings 物件只會在第一次呼叫時建立一次。✔️

from functools import lru_cache
from typing import Annotated

from fastapi import Depends, FastAPI

from . import config

app = FastAPI()


@lru_cache
def get_settings():
    return config.Settings()


@app.get("/info")
async def info(settings: Annotated[config.Settings, Depends(get_settings)]):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from functools import lru_cache

from fastapi import Depends, FastAPI

from . import config

app = FastAPI()


@lru_cache
def get_settings():
    return config.Settings()


@app.get("/info")
async def info(settings: config.Settings = Depends(get_settings)):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

之後在下一批請求的相依中任何對 get_settings() 的呼叫,都不會再執行 get_settings() 的內部程式碼與建立新的 Settings 物件,而是會一再回傳第一次呼叫時回傳的同一個物件。

lru_cache 技術細節

@lru_cache 會修改它所裝飾的函式,使其回傳第一次回傳的相同值,而不是每次都重新計算、執行函式碼。

因此,被裝飾的函式對於每組參數組合只會執行一次。之後,凡是以完全相同參數組合呼叫時,都會重複使用先前對應的回傳值。

例如,如果你有一個函式:

@lru_cache
def say_hi(name: str, salutation: str = "Ms."):
    return f"Hello {salutation} {name}"

你的程式可能會這樣執行:

sequenceDiagram

participant code as Code
participant function as say_hi()
participant execute as Execute function

    rect rgba(0, 255, 0, .1)
        code ->> function: say_hi(name="Camila")
        function ->> execute: execute function code
        execute ->> code: return the result
    end

    rect rgba(0, 255, 255, .1)
        code ->> function: say_hi(name="Camila")
        function ->> code: return stored result
    end

    rect rgba(0, 255, 0, .1)
        code ->> function: say_hi(name="Rick")
        function ->> execute: execute function code
        execute ->> code: return the result
    end

    rect rgba(0, 255, 0, .1)
        code ->> function: say_hi(name="Rick", salutation="Mr.")
        function ->> execute: execute function code
        execute ->> code: return the result
    end

    rect rgba(0, 255, 255, .1)
        code ->> function: say_hi(name="Rick")
        function ->> code: return stored result
    end

    rect rgba(0, 255, 255, .1)
        code ->> function: say_hi(name="Camila")
        function ->> code: return stored result
    end

在我們的相依 get_settings() 這個案例中,該函式甚至不帶任何參數,因此它總是回傳相同的值。

如此一來,它的行為幾乎就像全域變數。但因為它使用相依函式,因此我們可以在測試時輕鬆將其覆寫。

@lru_cachefunctools 的一部分,而 functools 是 Python 標準程式庫的一部分。你可以在Python 文件中閱讀 @lru_cache 以了解更多。

回顧

你可以使用 Pydantic Settings 來處理應用程式的設定或組態,並享有 Pydantic model 的全部能力。

  • 透過相依可以讓測試更容易。
  • 你可以搭配 .env 檔使用。
  • 使用 @lru_cache 可以避免每個請求都重複讀取 dotenv 檔,同時仍可在測試時覆寫設定。