← 中文资源

简体中文版本

深层合并分层配置映射

按默认值、文件、环境变量顺序递归合并嵌套配置,同时保持默认配置不被修改。

阅读英文原文

功能说明

这段代码实现配置分层合并:默认配置提供兜底值,文件配置覆盖部分字段,环境变量再覆盖最高优先级字段。与浅合并不同,嵌套字典会递归合并,因此只写 server.port 不会把整个 server 段替换掉。

它在加载 YAML、JSON 或 TOML 配置后特别有用,可以让每一层只声明自己关心的键,同时保留其他默认值。合并过程使用深拷贝,调用方可以安全复用原始字典。

关键代码

deep_merge 先用 deepcopy(dict(base)) 复制基础层,然后遍历覆盖层。若当前值和覆盖值都是 Mapping,递归合并;否则用 deepcopy(value) 直接替换。这样字典递归、标量和列表整体替换,语义清晰。

resolve_layers 按传入顺序从左到右折叠,后面的层优先级更高。main 用默认值、文件配置、环境配置三层演示,并断言结果与默认值未被污染。

使用注意

列表采用“整体替换”而不是拼接:features 会被文件层完全覆盖。如果业务需要合并列表,应显式定义去重或追加策略,不能依赖默认行为。None 会作为普通值覆盖旧值,无法表达“删除该键”;需要删除语义时要额外约定。

函数不检测循环引用,深层嵌套或自引用结构可能导致递归过深。键类型假定为字符串,混用其他类型可能让类型检查失效。配置中可能包含密钥,打印合并结果前应脱敏。

验证方式

示例断言最终 host 为 0.0.0.0、port 为 9000、features 为 ["search", "export"],同时默认配置中的 port 仍为 8000。可以再加入一层把 server 设为 None,观察结果变为 None,从而确认替换语义。

代码示例

from __future__ import annotations

from collections.abc import Mapping
from copy import deepcopy
from typing import Any


def deep_merge(
    base: Mapping[str, Any],
    override: Mapping[str, Any],
) -> dict[str, Any]:
    merged = deepcopy(dict(base))
    for key, value in override.items():
        current = merged.get(key)
        if isinstance(current, Mapping) and isinstance(value, Mapping):
            merged[key] = deep_merge(current, value)
        else:
            merged[key] = deepcopy(value)
    return merged


def resolve_layers(*layers: Mapping[str, Any]) -> dict[str, Any]:
    result: dict[str, Any] = {}
    for layer in layers:
        result = deep_merge(result, layer)
    return result


def main() -> None:
    defaults = {
        "server": {"host": "127.0.0.1", "port": 8000, "tls": False},
        "features": ["search"],
    }
    file_config = {"server": {"port": 9000}, "features": ["search", "export"]}
    env_config = {"server": {"host": "0.0.0.0"}}

    resolved = resolve_layers(defaults, file_config, env_config)
    print(resolved)
    assert resolved["server"]["port"] == 9000
    assert resolved["server"]["host"] == "0.0.0.0"
    assert resolved["features"] == ["search", "export"]
    assert defaults["server"]["port"] == 8000


if __name__ == "__main__":
    main()