背景与问题

  • 在开发中要尽量从环境变量、配置文件、数据库等读取,不能硬编码;
  • 需要灵活切换不同环境的配置,比如开发、测试、生产等。
  • 敏感数据存储需要考虑加密和权限控制,避免泄露。

为了避免上面的种种问题,通常有以下几种方案:

配置文件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智能提示、同时也方便切换不同环境的配置等。