Skip to content

设置和环境变量

在许多情况下,您的应用程序可能需要一些外部设置或配置,例如密钥、数据库凭证、电子邮件服务的凭证等。

其中大多数设置是可变的(可以更改),例如数据库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%

Info

在Pydantic v1中,它包含在主包中。现在它作为一个独立的包分发,因此如果您不需要该功能,可以选择是否安装它。

创建 Settings 对象

从Pydantic导入BaseSettings并创建一个子类,非常类似于Pydantic模型。

与Pydantic模型一样,您可以使用类型注释声明类属性,并可能提供默认值。

您可以使用与Pydantic模型相同的所有验证功能和工具,例如不同的数据类型和使用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,
    }

Info

在Pydantic v1中,您会直接从pydantic导入BaseSettings,而不是从pydantic_settings导入。

from fastapi import FastAPI
from pydantic 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,如您在更大的应用程序 - 多个文件中所见。

在依赖项中的设置

在某些情况下,从依赖项提供设置可能会有用,而不是使用一个全局对象settings,该对象在各处使用。 在测试过程中,这可能特别有用,因为用你自己的自定义设置覆盖依赖项非常容易。

配置文件

从前面的例子来看,你的 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()

主应用文件

现在我们创建一个依赖项,返回一个新的 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,
    }
from functools import lru_cache

from fastapi import Depends, FastAPI
from typing_extensions import Annotated

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,
    }

Tip

如果可能,建议使用 Annotated 版本。

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() 是一个普通函数。

然后我们可以将其作为依赖项从 路径操作函数 中请求,并在需要时在任何地方使用它。

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,
    }
from functools import lru_cache

from fastapi import Depends, FastAPI
from typing_extensions import Annotated

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,
    }

Tip

如果可能,建议使用 Annotated 版本。

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 创建依赖项覆盖,在测试期间提供不同的设置对象将非常容易:

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) support 中了解更多信息。

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")

Tip

model_config 属性仅用于 Pydantic 配置。你可以在 Pydantic: Concepts: Configuration 中了解更多信息。

from pydantic import BaseSettings


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

    class Config:
        env_file = ".env"

Tip

Config 类仅用于 Pydantic 配置。你可以在 Pydantic Model Config 中了解更多信息。

Info

在 Pydantic 版本 1 中,配置是在内部类 Config 中完成的,在 Pydantic 版本 2 中,它是在属性 model_config 中完成的。此属性接受一个 dict,为了获得自动补全和内联错误,你可以导入并使用 SettingsConfigDict 来定义该 dict

在这里,我们在 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 fastapi import Depends, FastAPI
from typing_extensions import Annotated

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,
    }
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,
    }

Tip

如果可能的话,建议使用 Annotated 版本。

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 模型的强大功能。

  • 通过使用依赖项,你可以简化测试。
  • 你可以使用 .env 文件。
  • 使用 @lru_cache 可以避免每次请求都重新读取 dotenv 文件,同时允许你在测试期间覆盖它。