4. 配置分层管理

配置分层管理 学习卡片

分层配置的加载顺序与覆盖规则

配置分层管理的核心是:越靠近运行现场的配置,优先级越高。本例采用四层结构,从低到高依次为:内置默认值 → 配置文件默认段 → 环境变量 → 本地私有配置文件。加载时逐层合并,高优先级层覆盖低优先级层的同名键。

配置分层加载与覆盖优先级 ① 内置默认值 (defaults) 代码中硬编码的兜底配置,保证任何环境下程序都能启动 例:host=127.0.0.1, port=8000, debug=False, db_url=sqlite:///app.db ② 配置文件默认段 (config.toml / settings.yaml) 随代码仓库分发的公共配置,定义各环境共享的基准值 例:[default] port=8080, log_level=INFO, db_url=postgresql://localhost/prod ③ 环境变量 (APP_*) 部署时注入,用于区分开发/测试/生产环境,不进入版本控制 例:APP_PORT=9090, APP_DEBUG=true, APP_DB_URL=postgresql://db:5432/app ④ 本地私有配置 (config.local.toml) 开发者本机覆盖,加入 .gitignore,优先级最高 例:port=3000, debug=true, db_url=sqlite:///dev.db 优先级递增 最终生效配置 = 逐层合并,高优先级覆盖同名键

实现分层加载器

下面实现一个 ConfigLoader 类,按上述四层顺序加载并合并配置。关键设计:每层返回一个字典,用 dict.update() 逐层覆盖;环境变量层通过前缀 APP_ 识别,并将 __ 转换为嵌套分隔符。

# config_loader.py
import os
import json
from pathlib import Path

DEFAULTS = {
    "host": "127.0.0.1",
    "port": 8000,
    "debug": False,
    "db_url": "sqlite:///app.db",
    "log_level": "INFO",
}

ENV_PREFIX = "APP_"

def load_file(path: str) -> dict:
    """加载 JSON 配置文件,文件不存在时返回空字典。"""
    p = Path(path)
    if not p.exists():
        return {}
    with open(p, encoding="utf-8") as f:
        return json.load(f)

def load_env(prefix: str = ENV_PREFIX) -> dict:
    """从环境变量提取配置,APP_PORT -> port, APP_DB__URL -> db.url"""
    result = {}
    for key, value in os.environ.items():
        if not key.startswith(prefix):
            continue
        # 去掉前缀,转小写,__ 转为嵌套分隔
        raw = key[len(prefix):].lower()
        parts = raw.split("__")
        # 尝试类型转换
        result[raw] = _coerce(value)
    return result

def _coerce(value: str):
    """将字符串转为 bool / int / float,失败则保留字符串。"""
    low = value.lower()
    if low in ("true", "false"):
        return low == "true"
    try:
        return int(value)
    except ValueError:
        pass
    try:
        return float(value)
    except ValueError:
        pass
    return value

def deep_merge(base: dict, override: dict) -> dict:
    """递归合并,override 覆盖 base 的同名键。"""
    merged = dict(base)
    for k, v in override.items():
        if k in merged and isinstance(merged[k], dict) and isinstance(v, dict):
            merged[k] = deep_merge(merged[k], v)
        else:
            merged[k] = v
    return merged

class ConfigLoader:
    def __init__(self, config_path: str = "config.json",
                 local_path: str = "config.local.json"):
        self.config_path = config_path
        self.local_path = local_path

    def load(self) -> dict:
        layers = [
            ("defaults", DEFAULTS),
            ("config_file", load_file(self.config_path)),
            ("env_vars", load_env()),
            ("local_file", load_file(self.local_path)),
        ]
        result = {}
        for name, layer in layers:
            result = deep_merge(result, layer)
        return result

验证覆盖优先级

创建两个配置文件模拟不同环境,然后通过环境变量注入覆盖值,观察最终结果是否符合预期。

# config.json —— 公共基准配置
{
  "port": 8080,
  "log_level": "INFO",
  "db_url": "postgresql://localhost/prod"
}
# config.local.json —— 开发者本机覆盖
{
  "port": 3000,
  "debug": true,
  "db_url": "sqlite:///dev.db"
}
# verify.py —— 验证脚本
import os
from config_loader import ConfigLoader

# 场景 A:无环境变量,仅默认 + 配置文件 + 本地文件
cfg_a = ConfigLoader().load()
print("场景A port:", cfg_a["port"])        # 期望 3000(本地文件覆盖)
print("场景A debug:", cfg_a["debug"])      # 期望 True(本地文件)
print("场景A log_level:", cfg_a["log_level"])  # 期望 INFO(仅配置文件有)

# 场景 B:注入环境变量 APP_PORT=9090
os.environ["APP_PORT"] = "9090"
cfg_b = ConfigLoader().load()
print("场景B port:", cfg_b["port"])        # 期望 9090(环境变量覆盖本地文件)
print("场景B debug:", cfg_b["debug"])      # 期望 True(本地文件仍生效)

# 场景 C:注入 APP_DEBUG=false 覆盖本地文件的 true
os.environ["APP_DEBUG"] = "false"
cfg_c = ConfigLoader().load()
print("场景C debug:", cfg_c["debug"])      # 期望 False(环境变量覆盖本地文件)
验证要点:场景 A 验证本地文件覆盖配置文件;场景 B 验证环境变量覆盖本地文件;场景 C 验证环境变量可以反向覆盖本地文件的布尔值。三个场景覆盖了四层中所有相邻层的覆盖关系。

各层覆盖关系速查

配置键① 默认值② config.json③ 环境变量④ config.local.json最终生效
port80008080—30003000
debugFalse——TrueTrue
log_levelINFOINFO——INFO
db_urlsqlite:///app.dbpostgresql://localhost/prod—sqlite:///dev.dbsqlite:///dev.db
port(场景B)80008080909030009090
debug(场景C)False—falseTrueFalse
常见陷阱:环境变量中的 APP_DEBUG=false 如果直接当字符串处理,在 Python 中 bool("false") 为 True,会导致覆盖失效。必须在 _coerce() 中显式处理布尔字符串。同理,APP_PORT=9090 需要转为 int,否则后续端口校验或拼接会出错。