跳至内容

设置和环境变量

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

大多数这些设置是可变的(可以更改),例如数据库 URL。许多可能是敏感的,例如密钥。

出于这个原因,通常在应用程序读取的环境变量中提供它们。

提示

要了解环境变量,你可以阅读 环境变量

类型和验证

这些环境变量只能处理文本字符串,因为它们是 Python 外部的,并且必须与其他程序和系统其余部分(甚至与不同的操作系统,例如 Linux、Windows、macOS)兼容。

这意味着从环境变量中在 Python 中读取的任何值都将是 str,并且任何类型转换或任何验证都必须在代码中完成。

Pydantic Settings

幸运的是,Pydantic 提供了一个很棒的实用程序来使用 Pydantic:设置管理 处理来自环境变量的这些设置。

安装 pydantic-settings

首先,确保你创建了 虚拟环境,激活它,然后安装 pydantic-settings

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

它也包含在使用以下命令安装 all 扩展时:

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

信息

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

信息

在 Pydantic v1 中,你将直接从 pydantic 而不是从 pydantic_settings 导入 BaseSettings

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

提示

如果你想要一些可以复制粘贴的内容,不要使用这个示例,使用下面最后一个示例。

然后,当你创建该 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)

提示

要为单个命令设置多个环境变量,只需用空格分隔它们,并将它们全部放在命令之前。

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

提示

你还需要一个__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,
    }

提示

如果可能,建议使用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,
    }

提示

我们将在后面讨论@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,
    }

提示

如果可能,建议使用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”。

提示

在类Unix系统(如Linux和macOS)中,以点(.)开头的文件是隐藏文件。

但dotenv文件并不一定非得有这个确切的文件名。

Pydantic支持使用外部库从这些类型的文件读取。你可以在Pydantic 设置:Dotenv (.env) 支持中了解更多信息。

提示

要使此方法起作用,你需要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")

提示

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

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"

提示

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

信息

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

提示

如果可能,建议使用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的一部分,它是Python标准库的一部分,你可以在Python 文档中关于@lru_cache的内容中了解更多信息。

回顾

你可以使用Pydantic Settings来处理应用程序的设置或配置,并充分利用Pydantic模型的强大功能。

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