跳转至

设置与环境变量

有时,我们要在应用外部对密钥、数据库凭证、电子邮件服务凭证等进行设置或配置。

这些设置大多数都是数据库 URL 等(可修改的)变量。有一些变量则包含密钥等敏感信息。

因此,FastAPI 应用通常需要读取环境变量里的配置。

环境变量

提示

如果您已经了解环境变量,并知道怎么使用环境变量,可以跳过本章下面的内容。

环境变量(即 env var)存储在操作系统里,虽然环境变量在 Python 代码之外,但能被 Python 代码(或其他程序)读取。

没有 Python,也能在 Shell 里创建与使用环境变量。

// 创建 MY_NAME 环境变量
$ export MY_NAME="Wade Wilson"

// 其他程序也可以使用环境变量,例如:
$ echo "Hello $MY_NAME"

Hello Wade Wilson
// 创建 MY_NAME 环境变量
$ $Env:MY_NAME = "Wade Wilson"

// 其他程序也可以使用环境变量,例如:
$ echo "Hello $Env:MY_NAME"

Hello Wade Wilson

Python 读取环境变量

使用 Terminal 命令窗口等方式在 Python 外部创建环境变量,再在 Python 里读取环境变量。

以如下的 main.py 为例:

import os

name = os.getenv("MY_NAME", "World")
print(f"Hello {name} from Python")

提示

os.getenv() 的第二个参数是要返回的默认值。

如果没有提供这个参数,则默认为 None,此处使用 World 作为默认值。

然后,调用 Python 程序:

// 此时还没有设置环境变量
$ python main.py

// 因为尚未设置环境变量,所以返回默认值

Hello World from Python

// 但如果事先创建了环境变量
$ export MY_NAME="Wade Wilson"

// 然后再调用程序
$ python main.py

// 现在,程序就能读取环境变量了

Hello Wade Wilson from Python

环境变量是在代码之外设置的,代码可以读取,但不能把环境变量与其他文件一起保存(用 git 提交),因此,这种方式常用于配置或设置。

您还可以创建仅供特定程序调用的环境变量,即专供指定程序使用,而且只能在程序运行期间使用。

为此要在同一命令行中,在调用程序之前创建环境变量:

// 在调用程序的同一命令行中创建环境变量 MY_NAME
$ MY_NAME="Wade Wilson" python main.py

// 现在,程序就能够读取环境变量了

Hello Wade Wilson from Python

// 程序运行完毕后,环境变量就不存在了
$ python main.py

Hello World from Python

类型与验证

环境变量只能处理文本字符串,因为它们在 Python 外部,必须兼容其他程序和操作系统组件,甚至还要兼容 Linux、Windows、macOS 等操作系统。

即,Python 从环境变量中读取的值必须是字符串,类型转换与验证等操作只能在代码中完成。

Pydantic 的 Settings

还好 Pydantic 提供了处理环境变量设置的工具,详见 Pydantic 官档 - 设置管理

创建 Settings 对象

从 Pydantic 导入 BaseSettings 并创建子类,这与创建 Pydantic 模型类似。

创建 Settings 对象和 Pydantic 模型一样,可以使用类型注解声明类的属性,并设置默认值。

还可以使用 Pydantic 模型的验证功能与工具,比如声明不同数据类型或使用 Field() 实现附加验证操作。

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

app 中使用新的 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,
    }

运行服务器

使用环境变量传递配置参数并运行服务器,以如下方式设置 ADMIN_EMAILAPP_NAME

$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" uvicorn main:app

<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 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 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 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 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 文件

如果经常要在不同环境下改变很多设置,最好把这些设置放到一个文件里,并从文件中以环境变量的形式读取设置内容。

这种做法很常见,通常会把设置变量放在 dotenv.env)文件里。

提示

以点(.)开头的文件在 Linux 和 macOS 等 Unix 系统里是隐藏文件。

但是 dotenv 文件实际上不必有明确的文件名。

Pydantic 支持使用外部支持库读取这种类型的文件。详见 Pydantic 官档 - 设置:Dotenv (.env) 支持

提示

使用这个功能需要先安装 pip install python-dotenv

.env 文件

假设 .env 文件内容如下:

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

读取 .env 中的设置

然后更新 config.py

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"

此处,创建 Pydantic Settings类中的 Config 类,并使用 env_file 设置 dotenv 文件名。

提示

Config 类只用于 Pydantic 配置。详见 Pydantic 模型配置

使用 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 . 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_cache() 是 Python 标准库 functools 的组件,详见 Python 官档 - @lru_cache()

小结

充分利用 Pydantic 模型的优势,使用 Pydantic 的 Settings 处理应用的设置或配置。

  • 通过依赖项简化测试
  • 支持使用 .env
  • 使用 @lru_cache(),不用每次请求时都读取 dotenv 文件,并且支持测试时覆盖使用