设置和环境变量¶
在许多情况下,应用程序可能需要一些外部设置或配置,例如密钥、数据库凭据、电子邮件服务凭据等。
这些设置大多是可变的(会发生变化),例如数据库 URL。许多设置可能是敏感的,例如密钥。
因此,通常的做法是将它们放入应用程序读取的环境变量中。
提示
要了解环境变量,可以阅读 环境变量。
类型与验证¶
这些环境变量只能处理文本字符串,因为它们对 Python 来说是外部的,并且必须与其它程序和系统的其余部分(甚至包括不同的操作系统,如 Linux、Windows、macOS)兼容。
这意味着在 Python 中从环境变量读取的任何值都将是 str,任何转换为不同类型的操作或任何验证都必须在代码中完成。
Pydantic Settings¶
幸运的是,Pydantic 提供了一个很棒的工具来处理来自环境变量的这些设置,即 Pydantic: Settings management。
安装 pydantic-settings¶
首先,确保创建了虚拟环境,激活它,然后安装 pydantic-settings 包
$ pip install pydantic-settings
---> 100%
当你通过以下方式安装 all 扩展包时,它也包含在内
$ pip install "fastapi[all]"
---> 100%
创建 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,
}
提示
如果你想要快速复制粘贴的内容,请不要使用此示例,请使用下方的最后一个示例。
然后,当你创建该 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_EMAIL 和 APP_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
🤓 其他版本和变体
提示
如果可能,请优先使用 Annotated 版本。
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,
}
🤓 其他版本和变体
提示
如果可能,请优先使用 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,
}
🤓 其他版本和变体
提示
如果可能,请优先使用 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,
}
🤓 其他版本和变体
提示
如果可能,请优先使用 Annotated 版本。
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 Settings: Dotenv (.env) support。
提示
要使其工作,你需要 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")
🤓 其他版本和变体
提示
如果可能,请优先使用 Annotated 版本。
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: Concepts: Configuration。
在这里,我们在你的 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,
}
🤓 其他版本和变体
提示
如果可能,请优先使用 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_cache 是 Python 标准库中 functools 的一部分,你可以在 Python 官方文档中关于 @lru_cache 的部分 阅读更多相关信息。
总结¶
你可以使用 Pydantic Settings 来处理应用程序的设置或配置,并享有 Pydantic 模型的所有强大功能。
- 通过使用依赖项,你可以简化测试。
- 你可以将
.env文件与之配合使用。 - 使用
@lru_cache可以让你避免为每个请求重复读取 dotenv 文件,同时允许你在测试期间覆盖它。