背景与问题
- 在开发中要尽量从环境变量、配置文件、数据库等读取,不能硬编码;
- 需要灵活切换不同环境的配置,比如开发、测试、生产等。
- 敏感数据存储需要考虑加密和权限控制,避免泄露。
为了避免上面的种种问题,通常有以下几种方案:
配置文件config.py
这种方式尽量不要使用,硬编码导致维护困难,也造成环境切换的麻烦,除非特别的情况,比如常量、固定的路径等。也尽量写在配置文件等;
# config.py
# 数据库配置
DB_HOST = 'localhost'
DB_PORT = 3306
DB_USER = 'root'
DB_PASSWORD = '123456'
DB_NAME = 'myapp'
dotenv
dotenv 是一个 Python 库,用于从 .env 文件加载环境变量。它可以帮助我们避免在代码中硬编码敏感信息,同时也方便切换不同环境的配置。
pip install python-dotenv
配置文件 .env 示例:
# .env
# 数据库配置
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=123456
DB_NAME=myapp
读取配置文件:
# app.py
from dotenv import load_dotenv
load_dotenv() # 加载 .env 文件中的环境变量
print(os.getenv('DB_HOST')) # 输出:localhost
print(os.getenv('DB_PORT')) # 输出:3306
print(os.getenv('DB_USER')) # 输出:root
print(os.getenv('DB_PASSWORD')) # 输出:123456
print(os.getenv('DB_NAME')) # 输出:myapp
灵活切换不同的env配置文件,比如开发、测试、生产等。兜底默认读取 .env 文件。运行的时候可以注入 ENV_NAME 环境变量来切换不同的配置文件。
如:ENV_NAME=dev python app.py 会加载 config/.dev.env 文件中的环境变量。
import os
from dotenv import load_dotenv
env_suffix=os.getenv("ENV_NAME")
# 打印环境变量名称
print(os.getenv("ENV_NAME"))
if env_suffix:
load_dotenv(f"config/.{env_suffix}.env")
else:
load_dotenv("config/.env")
# 打印环境变量值
print(os.getenv("env_name"))
pydantic_settings
推荐使用:pydantic_settings 是一个 Python 库,用于从环境变量和配置文件中加载设置。它的优势相比上面的方案,是可以帮助我们定义和验证设置、类型检查、IDE智能提示、同时也方便切换不同环境的配置等。
pip install pydantic-settings
方法一: 设置settings类,从环境变量和配置文件中加载设置。验证设置的类型和值。
import os
from pathlib import Path
from pydantic_settings import BaseSettings
from pydantic import Field, field_validator
CONFIG_DIR = Path(__file__).parent.parent / "env"
ENV_CODE = os.getenv("ENV_CODE", "dev")
env_file_path = CONFIG_DIR / f".{ENV_CODE}.env"
print(f"Loading environment file: {env_file_path}")
class Settings(BaseSettings):
# 直接使用与环境变量文件匹配的字段名
env_name: str = Field(default="dev")
version: str = Field(default="0.0.1")
db_host: str = Field(default="db_host")
db_port: int = Field(default=5432) # 建议使用 int 类型
db_user: str = Field(default="app_user")
db_password: str = Field(default="strong_dev_password")
db_name: str = Field(default="myapp_dev")
# 如果需要 Redis 配置
redis_host: str = Field(default="redis_host")
redis_port: int = Field(default=6379)
redis_password: str = Field(default="redis_password")
@field_validator("version")
def version_validator(cls, v):
return f"v{v}"
# 属性方式访问数据库配置(可选)
@property
def database_url(self) -> str:
return f"postgresql://{self.db_user}:{self.db_password}@{self.db_host}:{self.db_port}/{self.db_name}"
@property
def redis_url(self) -> str:
return f"redis://:{self.redis_password}@{self.redis_host}:{self.redis_port}"
class Config:
env_file = env_file_path
env_file_encoding = "utf-8"
extra = "allow" # 允许未定义的字段
settings = Settings()
# 使用时直接访问
print(settings.version) # 输出: v1.0.1
print(settings.db_host) # 输出: dev-db.example.com
print(settings.database_url) # 输出数据库连接字符串
env就正常加载:
env_name=开发环境
version=1.0.1
# 数据库配置
db_host=dev-db.example.com
db_port=5432
db_user=app_user
db_password=strong_dev_password
db_name=myapp_dev
方法二: 在方法一的基础上,配置成多个类 AppSettings 和 DatabaseSettings等等,在Settings中注册使用,每个类的字段都可以有默认值,也可以从环境变量中加载。
# settings.py
... ...
class AppSettings(BaseModel):
env_name: str = Field(default="dev")
version: str = Field(default="0.0.1")
@field_validator("version")
def version_validator(cls, v):
return f"v{v}"
class Settings(BaseSettings):
db_settings: DatabaseSettings = DatabaseSettings()
app_settings: AppSettings = AppSettings()
# pydantic-settings 2.0之前写法
class Config:
env_file = env_file_path
env_file_encoding = "utf-8"
env_nested_delimiter = "__" # 使用双下划线分隔嵌套字段
extra = "allow"
# pydantic-settings 2.0之后写法 使用 SettingsConfigDict
# model_config = SettingsConfigDict(
# env_file=".env",
# extra="allow",
# )
这时候env文件要命名成统一前缀,感觉配置繁琐
# .env.dev
# app_settings__ 前缀
app_settings__env_name=开发环境
app_settings__version=1.0.1
读取的时候使用
from config.settings import settings
print(settings.app_settings.version)
🏁 改进版: 配置依旧如上,但是可以导出各个模块的配置来引用。
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field, validator, field_validator
class DatabaseConfig(BaseSettings):
"""数据库配置"""
DATABASE_URL: str = Field(..., description="数据库连接URL")
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore"
)
class LLMConfig(BaseSettings):
"""大语言模型配置"""
DASHSCOPE_API_KEY: str = Field(..., description="DashScope API密钥")
DASHSCOPE_API_URL: str = Field(
default="https://dashscope.aliyuncs.com/compatible-mode/v1",
description="DashScope API地址"
)
DASHSCOPE_LLM_MODEL: str = Field(
default="qwen3-max-2026-01-23",
description="文本大模型名称"
)
DASHSCOPE_EMD_MODEL: str = Field(
default="qwen3-embedding-2026-01-23",
description="向量模型名称"
)
# 验证API密钥格式
@field_validator('DASHSCOPE_API_KEY')
def validate_api_key(cls, v):
if not v or not v.startswith('sk-'):
raise ValueError('API密钥格式不正确,应该以sk-开头')
return v
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore"
)
class AppConfig(BaseSettings):
"""应用配置"""
DEBUG: bool = Field(default=False, description="调试模式")
APP_NAME: str = Field(default="Law Chat Bot", description="应用名称")
APP_VERSION: str = Field(default="1.0.0", description="应用版本")
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore"
)
class Settings(BaseSettings):
"""全局配置类"""
# 嵌套配置
database: DatabaseConfig = Field(default_factory=DatabaseConfig)
llm: LLMConfig = Field(default_factory=LLMConfig)
app: AppConfig = Field(default_factory=AppConfig)
# 全局配置项
ENVIRONMENT: str = Field(default="development", description="运行环境")
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore"
)
@field_validator('ENVIRONMENT')
def validate_environment(cls, v):
allowed_environments = ['development', 'testing', 'production']
if v not in allowed_environments:
raise ValueError(f'环境必须是: {allowed_environments}')
return v
# 全局配置实例(单例模式)
_settings_instance = None
def get_settings() -> Settings:
"""获取全局配置实例(单例)"""
global _settings_instance
if _settings_instance is None:
# 确保环境变量被加载
from dotenv import load_dotenv
import os
# 显式加载.env文件
env_file = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
if os.path.exists(env_file):
load_dotenv(env_file)
else:
# 如果找不到.env文件,尝试在当前目录找
load_dotenv()
_settings_instance = Settings()
return _settings_instance
def get_database_config() -> DatabaseConfig:
"""获取数据库配置"""
return get_settings().database
def get_llm_config() -> LLMConfig:
"""获取LLM配置"""
return get_settings().llm
def get_app_config() -> AppConfig:
"""获取应用配置"""
return get_settings().app
# 导出全局配置实例
settings = get_settings()
根据这样的配置,可以使用
` config = get_llm_config() 来单独使用某个模块
也可以使用from app.core.config import settings` 来导入全局配置
总结
在配置和读取环境变量的时候,我们一般都使用env文件来配置变量,可以直接使用dotenv库来加载。而pydantic_settings库则可以帮助我们定义和验证设置、类型检查、IDE智能提示、同时也方便切换不同环境的配置等。