游乐游手机版
首页/AI教程/文章详情

Python字典合并避坑指南:三天踩坑经验总结

时间:2026-07-07 17:48
Python字典的内置合并方法均执行浅层合并,嵌套字典中的子键会被整块覆盖而非递归合并。处理多层配置时需自定义递归函数或使用deepmerge库,否则易导致数据丢失,如update()、解包、|运算符及ChainMap均存在此问题。

一个让我在配置系统上翻车的Bug

去年我在开发一个微服务的配置管理模块时,需要将用户的个性化配置与系统默认配置合并到一起。当初的代码大致是这样的:

default_config = {
"database": {
"host": "localhost",
"port": 5432,
"pool_size": 10
},
"logging": {
"level": "INFO",
"file": "/var/log/app.log"
}
}

user_config = {
"database": {
"port": 5433,
"pool_size": 20
}
}

# 合并配置——当初是这么写的
default_config.update(user_config)
print(default_config)

我当时期望的输出是:

{
"database": {
"host": "localhost", # 保留默认的host
"port": 5433, # 用户覆盖了port
"pool_size": 20 # 用户覆盖了pool_size
},
"logging": {
"level": "INFO",
"file": "/var/log/app.log"
}
}

但实际输出却是:

{
"database": {
"port": 5433,
"pool_size": 20
# host呢???
},
"logging": {
"level": "INFO",
"file": "/var/log/app.log"
}
}

host居然消失了!

我盯着屏幕看了整整十分钟,脑子里只有一个疑问:为什么update()把整个嵌套字典整块替换掉了,而不是递归合并?

这个Bug花了我三天才彻底搞明白。今天就把Python字典合并的各种陷阱一次性讲清楚,希望能帮你省下这三天时间。

坑一:update()的“浅层覆盖”——嵌套字典整块消失

dict.update()是Python里最常用的字典合并方法,但它的行为是浅层更新(shallow update)。当某个键对应的值是嵌套字典时,它会直接替换整个子字典,而不是递归合并其内部内容。

d1 = {"a": {"x": 1, "y": 2}}
d2 = {"a": {"z": 3}}
d1.update(d2)
print(d1) # {"a": {"z": 3}} —— x和y全丢了!

这就是那个配置系统里host消失的根本原因。update()只看到database这个键存在,就直接把整个database子字典替换成了用户配置里的那个子字典,默认配置中的host自然就没了。

为什么Python要这样设计?

因为字典在Python里被视为扁平的键值映射,update()遵循“键存在则覆盖”的原则,并不预设嵌套结构的语义。此外,递归遍历所有层级会引入不必要的开销,这与update()追求高效原地更新的初衷相违背。

解决方案:

如果确实需要深度合并嵌套字典,必须自己写递归函数:

def deep_merge(dict1, dict2):
"""递归合并两个字典,dict2的值优先"""
result = dict1.copy()
for key, value in dict2.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = deep_merge(result[key], value)
else:
result[key] = value
return result

d1 = {"a": {"x": 1, "y": 2}}
d2 = {"a": {"z": 3}}
merged = deep_merge(d1, d2)
print(merged) # {"a": {"x": 1, "y": 2, "z": 3}} —— 完美!

或者直接使用第三方库,比如deepmerge

坑二:update()会原地修改——你的原始数据被污染了

update()的另一个特点是原地修改——它会直接修改调用它的字典,而不是返回一个新字典。

defaults = {"theme": "dark", "language": "en"}
user_settings = {"language": "zh"}

defaults.update(user_settings) # defaults被直接修改了!
print(defaults) # {"theme": "dark", "language": "zh"}

# 如果后面其他地方还想用原始的defaults,就出问题了

很多人在配置合并场景中习惯写defaults.update(user_config),结果后续其他模块读取defaults时拿到的就是已被污染的值。

正确做法:先复制再更新。

safe_settings = defaults.copy()
safe_settings.update(user_settings)

坑三:解包语法{**d1, **d2}——看起来很美,但坑一样在

Python 3.5引入了**解包运算符用于字典合并,语法非常简洁:

merged = {**default_config, **user_config}

优点是不会修改原字典,而是返回一个新字典。但嵌套字典的覆盖问题依然存在。

d1 = {"a": {"x": 1, "y": 2}}
d2 = {"a": {"z": 3}}
merged = {**d1, **d2}
print(merged) # {"a": {"z": 3}} —— x和y还是丢了!

解包只是将两个字典的顶层键值对合并到一起,遇到嵌套字典照样直接替换。

注意:{**d1, **d2}d1.update(d2)在键冲突时的行为完全一致——后者覆盖前者。如果两个字典有同名键,最终结果始终以右侧字典的值为准。

坑四:|运算符(Python 3.9+)——新语法,老问题

Python 3.9通过PEP 584引入了||=运算符。

# Python 3.9+
merged = d1 | d2 # 返回新字典
d1 |= d2 # 原地更新,相当于d1.update(d2)

语法确实优美,PEP的作者形容这是为了解决过去合并字典时感受到的“痛苦”。

但嵌套覆盖的问题依然存在:

d1 = {"a": {"x": 1}}
d2 = {"a": {"y": 2}}
merged = d1 | d2
print(merged) # {"a": {"y": 2}} —— x又丢了

|运算符只做浅层键值叠加,不会递归处理嵌套结构。

另外需要注意:字典联合不符合交换律——d | ee | d的结果可能完全不同,因为右侧字典的键值会覆盖左侧的。

d1 = {"a": 1, "b": 2}
d2 = {"b": 3, "c": 4}
print(d1 | d2) # {"a": 1, "b": 3, "c": 4}
print(d2 | d1) # {"b": 2, "c": 4, "a": 1} —— 结果不同!

坑五:ChainMap的“假合并”——你以为合并了,其实没有

collections.ChainMap可以把多个字典在逻辑上变成一个字典。

from collections import ChainMap
a = {"a": 1, "b": 2}
b = {"b": 3, "c": 4}
c = ChainMap(a, b)
print(c["a"]) # 1
print(c["b"]) # 3 —— 来自第一个字典a

但它并不会真的把字典合并在一起,而是在内部储存一个Key到每个字典的映射。读取时先去第一个字典找,找不到才去第二个。

隐藏坑1:修改ChainMap会影响原始字典。

c["new_key"] = 100
print(a) # {"a": 1, "b": 2, "new_key": 100} —— a被改了!

所有写操作([]=delpopclear)都只修改maps[0],也就是第一个字典。

隐藏坑2:如果同一个Key在多个字典中存在,从ChainMap删除它只会从第一个字典中删除。

隐藏坑3:ChainMap存储的是对原始字典的引用,而不是拷贝。修改原始字典,ChainMap会同步变化。

a["a"] = 999
print(c["a"]) # 999 —— 跟着变了

因此,ChainMap更适合只读场景(比如多层配置的查找),不适合真正需要合并出一个独立字典的场景。

一张表总结五种合并方式

方法是否修改原字典嵌套字典是否递归合并适用场景d1.update(d2)✅ 是❌ 否(直接覆盖)原地更新,不关心原数据被改{**d1, **d2}❌ 否❌ 否(直接覆盖)需要新字典,Python 3.5+d1 | d2❌ 否(|) / ✅ 是(|=)❌ 否(直接覆盖)Python 3.9+,函数式风格ChainMap(d1, d2)❌ 否(逻辑视图)不适用(不真正合并)只读的多层配置查找自定义deep_merge取决于实现✅ 是需要深度合并嵌套字典","rows":6,"cols":4,"id":"gTyiy"}">

回到开头的Bug

那个配置系统最后采用了自定义的deep_merge函数。用户配置只覆盖自己指定的字段,默认配置里没被覆盖的字段全部保留。

final_config = deep_merge(default_config, user_config)
# database.host 保住了,port和pool_size被用户覆盖了

三天时间没有白费——从此我再也没被字典合并坑过。

记住:Python内置的字典合并方法都是浅合并。如果你处理的是嵌套字典(配置文件、JSON数据、API响应),默认情况下嵌套层会被整块覆盖,而不是递归合并。需要深度合并?自己写递归函数,或者用deepmerge库。

希望你能少加三天班。

来源:https://developer.aliyun.com/article/1745932
上一篇AutoMem:可训练的智能体记忆管理技能 下一篇告别手动PPT,AI一句话3分钟生成完整演示文稿
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
科研人员必读:多肽、蛋白质、重组蛋白区别及定制指南
AI教程 · 2026-07-07

科研人员必读:多肽、蛋白质、重组蛋白区别及定制指南

Section 01 多肽 VS 蛋白质 VS 重组蛋白 多肽、蛋白质和重组蛋白,本质上是同宗同源的东西——都是氨基酸串起来的生物大分子。三者的核心区别,说到底无非是三个维度:分子大小、折叠形态,以及生产方式。 接下来是一张清晰的对比图,帮你快速建立直觉: ![对比图1](https:

知识图谱与本体语义建模的核心区别解析
AI教程 · 2026-07-07

知识图谱与本体语义建模的核心区别解析

谈到人工智能如何“理解”知识,有两个概念常被放在一起讨论:知识图谱与本体语义建模。不少人以为它们是同一事物,或者认为后者是前者的进化版。实际上,两者的分工完全不同——打个比方,一个是“记事的本子”,另一个是“写本子之前先定好的规矩”。 1 本体语义建模:先绘制一张“通用分类蓝图” 设想一下,你要整

强烈推荐工作搭子WorkBuddy
AI教程 · 2026-07-07

强烈推荐工作搭子WorkBuddy

一次偶然的机会,从朋友那里了解到WorkBuddy这个工具。说实话,在AI产品扎堆的今天,能遇到一个下载即用的助手,确实值得推荐给每一个被日常琐事缠身的人。 安装过程没什么难度,双击安装包默认安装即可。需要留意的是,如果在Windows7上折腾了半天没反应,别慌——这工具在高版本Windows下运行

跨境电商系统自动化测试与CI/CD流水线构建指南
AI教程 · 2026-07-07

跨境电商系统自动化测试与CI/CD流水线构建指南

技术方向:自动化测试与DevOps实践关键词:日本代购、一站式日淘、雅虎代拍系统、煤炉自动代拍 一、测试分层策略详解 不少人刚开始就想直接搞E2E测试,觉得跑通完整流程才够“真实”。然而,测试金字塔这么多年仍不过时,原因很简单——不同层级的测试各有分工,缺少任何一层都会不稳。来看看这张金字塔图: ┌

中小企业AI营销矩阵工具推荐:赛诺贝斯智域蒲公英
AI教程 · 2026-07-07

中小企业AI营销矩阵工具推荐:赛诺贝斯智域蒲公英

天天刷着别人的爆款内容,自己却“有心无力”——这才是2026年绝大多数中小企业运营社交媒体的真实写照。说白了,社交媒体如今早已不是“要不要做”的选择题,而是“怎么做才能真正见效”的生存考验。现实情况是,团队人力就那么几个,预算也紧巴巴,却要同时运营抖音、小红书、知乎、头条、百家号等多个阵地……文案、