Python的字典合并坑了我三天,原来是踩了这个坑

在线体验各类最新模型,更有模型 免费Token 额度领取!
立即体验
简介: 本文揭秘Python字典合并的五大陷阱:`update()`浅层覆盖导致嵌套键丢失、原地修改污染原始数据、解包`{**d1,**d2}`和`|`运算符同样不递归、`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让我花了三天才彻底搞明白。今天我把字典合并的各种坑一次性讲清楚,希望能帮你省下这三天时间。

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (39).png


坑一: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 取决于实现 ✅ 是 需要深度合并嵌套字典

回到开头的Bug

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

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

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

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

希望你能少加三天班。

目录
相关文章
|
1天前
|
数据采集 JSON 文字识别
爬虫实战:如何优雅地抓取网页中隐藏在伪元素(::before)里的文本?
本文揭秘爬虫中“页面可见但代码抓不到”的元凶——CSS伪元素(::before/::after)。它生成的文本不在DOM中,导致requests+BS4失效。文章剖析content四种来源,提供接口直采、CSS正则解析、渲染引擎+代理IP三阶方案,并附Selenium实战代码与避坑指南。(239字)
|
1天前
|
安全 数据处理 Python
别让你的变量“迷路”:Python进程间通信的3种正确姿势
本文揭秘Python多进程数据共享的三大正确姿势:Queue(安全传递任务与结果)、Manager(共享列表/字典等复杂结构)、SharedMemory(高性能共享大数组)。通过真实案例剖析“子进程修改全局变量无效”的根源,并提供选型决策指南,助你避开进程间通信陷阱,提升程序性能。(239字)
31 0
|
1天前
|
人工智能 安全 JavaScript
欺诈即服务产业化下网络钓鱼攻击演化与分层防御技术研究
本文剖析欺诈即服务(FaaS)产业化趋势,揭示其如何将网络钓鱼转为标准化订阅服务,降低攻击门槛、加剧全球数据资产流失。研究构建URL识别、DOM检测、语义分类三层智能防御体系,并提供可落地Python代码,提出技术、监管、用户认知协同的闭环防控策略。(239字)
23 0
|
1天前
|
人工智能 安全 JavaScript
英国亚马逊 Prime 仿冒邮件钓鱼攻击机理与分层检测防御体系研究
本文以2026年英国埃塞克斯郡亚马逊Prime钓鱼案为样本,剖析仿冒会员通知类攻击全链路,指出传统黑名单与关键词过滤在形近域名、动态JS劫持、AI本土化邮件识别上存在明显短板。创新提出“邮件信源—URL识别—页面DOM检测—用户行为”四级分层防御架构,并提供三类可落地Python代码模块,实测识别准确率达93.7%,显著优于传统方案,为英方监管、电商及邮件服务商提供标准化技术与治理闭环。(239字)
38 0
|
1天前
|
自然语言处理 安全 网络安全
如何识别用户输入中的恶意诱导和敏感指令?
企业 AIGC 应用识别恶意诱导和敏感指令,需要把“内容是否敏感”扩展为“意图是否危险、权限是否匹配、上下文是否可信、动作是否可控”。推荐采用输入检测、风险标签、策略引擎、权限校验、输出审核和审计回流的组合架构,避免仅依赖模型自身拒答或静态敏感词规则。
|
27天前
|
人工智能 自然语言处理 API
阿里云海外重磅发布 Qwen Cloud
Qwen Cloud,正是为AI Agent 而生的全新服务方式。
2380 61
|
3月前
|
网络协议 Linux 数据安全/隐私保护
Docker部署避坑:OpenClaw容器内无法使用代理?网络模式选择建议
本文详解OpenClaw在Docker中代理失效的5大典型坑:环境变量未透传、YAML与环境变量冲突、Docker Desktop网络隔离、host网络模式误用、DNS解析失败,并提供可直接复用的docker-compose配置模板。(239字)
448 0
|
2月前
|
存储 NoSQL 数据库
办公Agent的长期记忆设计:记住员工偏好(通知方式、常用模板)
本文揭秘如何为办公Agent设计靠谱的长期记忆系统:通过三层偏好模型(显式/隐式/场景化)、混合存储方案、智能更新遗忘机制及隐私保护设计,让Agent真正“记住你”——不再重复提问,主动适配习惯。不堆术语,只讲实战经验与避坑指南。(239字)
195 0
|
3月前
|
索引 Python Windows
别再手动调PPT了:用Python批量生成文本框,还能自动排版
小张苦于每周手动制作PPT周报。本文介绍如何用Python库`python-pptx`自动化生成格式统一的周报:从创建文本框、精细控制字体/颜色/对齐,到实现多级列表、模板占位符替换,助运营人员告别重复劳动,5分钟搞定周报。(239字)
334 0
|
3月前
|
数据采集 网络协议 测试技术
兼容性测试:OpenClaw配合SOCKS5代理的稳定性实测
本文实测站大爷SOCKS5代理在OpenClaw中的真实表现:连接成功率97.5%、支持UDP与全协议,但配置较复杂、偶发认证失败;相较HTTP代理(99.3%成功率、更稳更简),SOCKS5适合需UDP/FTP等进阶场景,新手推荐HTTP。
298 0