那个让我加班到凌晨的Bug
去年双十一,促销配置合并出错,优惠券全场通用。老板在群里@我,我汗流浃背地回滚。根因?两个字典用update()合并,第二个字典的默认值把第一个覆盖了。看似简单,暗藏杀机🤔
update():老派但危险
dict1.update(dict2)是最常见的合并方式。它是原地修改,返回None。很多新手写成new = dict1.update(dict2),结果new是None,dict1却被改了。这种静默污染,Debug的时候能让你当场崩溃😂
base = {'a': 1, 'b': 2}
extra = {'b': 99, 'c': 3}
# 错误的写法!
merged = base.update(extra)
print(merged) # None ☕
print(base) # {'a': 1, 'b': 99, 'c': 3} 被污染了!
| 运算符:Python3.9的语法糖
Python3.9引入了|合并运算符。dict1 | dict2返回新字典,不污染原数据。看起来很美好?别急着欢呼。它只支持dict | dict,**解包那种混合玩法不行。而且,也是浅拷贝。
# 干净的新字典
merged = base | extra
print(base) # {'a': 1, 'b': 2} 原数据安然无恙
# 链式合并
config = defaults | env_vars | cli_args
深拷贝:那个80%人踩过的坑
|和update()都是浅拷贝。字典的值如果是列表或字典,合并后两边共享同一个引用。改一个,另一个也变。我同事因为这个bug把用户权限表搞串了,差点被祭天💰
defaults = {'users': ['alice']}
config = defaults | {'users': defaults['users']}
config['users'].append('bob')
print(defaults['users']) # ['alice', 'bob'] 淦!
深拷贝的正确打开方式
用copy.deepcopy才是保命之道。但别无脑全量深拷贝,性能吃不起。我的做法是先浅拷贝,再对可疑的值按需深拷。精细化手术,不要大炮打蚊子。
import copy
defsafe_merge(d1, d2):
result = copy.deepcopy(d1)
for k, v in d2.items():
if isinstance(v, dict) and k in result:
result[k] = safe_merge(result[k], v)
else:
result[k] = copy.deepcopy(v)
return result
ChainMap:被忽略的轻量级方案
collections.ChainMap不合并,而是把多个字典链起来查询。查的时候按顺序找,找到就停。写多层级配置的时候超好用。而且它是零拷贝,性能爆炸。缺点是写操作只影响第一个字典。
from collections import ChainMap
defaults = {'theme': 'dark', 'lang': 'en'}
user_prefs = {'lang': 'zh'}
cli_args = {'debug': True}
config = ChainMap(cli_args, user_prefs, defaults)
print(config['lang']) # 'zh' 按优先级命中
print(config['theme']) # 'dark' fallback到defaults
合并策略对比表
选哪种方案?看场景。我整理了一张表,面试的时候背下来能装个大的😂
方案 是否新对象 深/浅拷贝 多字典链式 适用场景
update() 否(原地) 浅拷贝 否 临时补丁
| 运算符 是 浅拷贝 是 配置合并
dict(**a,**b) 是 浅拷贝 否 简单合并
ChainMap 否(视图) 引用 是 优先级查询
deepcopy 是 深拷贝 手动实现 数据隔离
说点得罪人的
看到有人用{**d1, **d2}就觉得自己很Pythonic。大哥,键冲突的时候后面覆盖前面,你确定这是你想要的行为?生产环境里,显式永远比隐式安全。别为了少写几个字符,埋个雷给后人踩☕
递归合并:处理嵌套字典
配置文件经常是嵌套结构。json文件加载出来,两层三层嵌套很正常。|和update()碰到嵌套直接覆盖外层,不会智能合并内层。这时候必须手写递归合并。
import copy
defdeep_merge(base, override):
result = copy.deepcopy(base)
for k, v in override.items():
if k in result and isinstance(result[k], dict) and isinstance(v, dict):
result[k] = deep_merge(result[k], v)
else:
result[k] = copy.deepcopy(v)
return result
base = {'db': {'host': 'localhost', 'port': 3306}}
override = {'db': {'port': 5432}}
print(deep_merge(base, override))
# {'db': {'host': 'localhost', 'port': 5432}}