在Python日常开发中,字典合并是一项出现频率很高的操作。无论是合并默认配置与用户配置、聚合多个数据查询结果,还是统一处理函数参数,开发者都需要把不同来源的键值对整合为一个字典。Python提供了update方法、|操作符以及**解包操作符等多种合并方式。它们虽然都能完成键值合并,但在返回值、是否修改原字典、版本兼容性以及代码可读性等方面存在明显差异,需要根据具体场景谨慎选择。

一、三种合并方式的基础用法
1. update方法合并
update方法是字典对象最基础的合并方式。它直接接收另一个字典作为参数,把后者的键值对写入调用对象。如果当前字典已经包含某个键,新值会覆盖旧值;如果键不存在,则相应键值对会被追加进去。由于该方法修改的是原字典本身,它不会返回新的字典,函数返回值是None。因此使用update时,重点应该放在执行后的原字典内容上,而不是函数调用的返回值上。
# 定义两个待合并的字典
employee_info = {'name': '张三', 'age': 30}
updates = {'age': 31, 'department': '研发部'}
# 调用update会直接修改employee_info
employee_info.update(updates)
print(employee_info) # 输出:{'name': '张三', 'age': 31, 'department': '研发部'}
2. | 操作符合并
|操作符是较新Python版本中为字典引入的合并语法。该操作符原本用于集合的并集运算,在字典场景下会返回一个全新的字典,其中包含两个字典的全部键值对。当键发生冲突时,右侧字典的值优先,这也符合从左到右的覆盖顺序。由于不会修改参与运算的原始字典,它更适合函数式风格的数据处理以及需要保留原始数据快照的场景。
# 定义两个配置字典
default_config = {'host': 'localhost', 'port': 8080}
user_config = {'port': 9090, 'debug': True}
# 使用|运算符生成新字典
merged = default_config | user_config
print(merged) # 输出:{'host': 'localhost', 'port': 9090, 'debug': True}
print(default_config) # 输出:{'host': 'localhost', 'port': 8080}
3. ** 操作符合并
**操作符通过字典解包来完成合并。把两个字典分别解包进一个新的字典字面量中,同样遵循后者覆盖前者的规则。这种方法并不是新引入的语法,在所有Python 3.x版本中都可以使用,因此在需要兼容旧环境的项目中更为稳妥。其本质是先把字典展开为键值对,再重新构造字典,这也便于在合并过程中插入额外的静态键值。
# 定义基础参数与覆盖参数
base_params = {'timeout': 5, 'retry': 3}
override_params = {'retry': 5, 'verbose': True}
# 使用**解包生成合并后的新字典
combined = {**base_params, **override_params}
print(combined) # 输出:{'timeout': 5, 'retry': 5, 'verbose': True}
print(base_params) # 输出:{'timeout': 5, 'retry': 3}
二、三种方式的核心差异对比
如果仅从表面上看,三种方式似乎都能完成相同的任务,但细节差异会直接影响程序行为。最容易混淆的是update方法与另外两种方式的返回值差异:update原地修改原字典并返回None,而|与**都会返回新字典且保留原字典。这个差异决定了变量赋值、链式调用以及数据不可变设计的可行性。
| 对比维度 | update方法 | | 操作符 | ** 操作符 |
|---|---|---|---|
| 返回值 | 无返回值(None) | 返回新的合并字典 | 返回新的合并字典 |
| 原字典是否修改 | 是,直接修改调用方法的字典 | 否,原字典保持不变 | 否,原字典保持不变 |
| 版本兼容性 | 所有Python版本支持 | 仅Python 3.9及以上版本支持 | 所有Python 3.x版本支持 |
| 重复键处理 | 后面的字典键覆盖前面的 | 后面的字典键覆盖前面的 | 后面的字典键覆盖前面的 |
从版本兼容性角度看,update方法和**解包在所有Python 3.x版本中都可使用,而|运算符需要Python 3.9或更高版本。重复键处理虽然三者的最终覆盖方向一致,但update是在已有字典上原地修改,后两者则是在构造新字典的过程中完成覆盖。理解这些维度的差异,可以避免把update的返回值误用为合并结果,也能帮助团队统一代码风格。
三、适用场景分析
在实际项目中,选择合并方式通常取决于三个问题:是否需要保留原字典、是否需要兼容低版本解释器,以及代码更看重可读性还是执行效率。这些问题没有唯一答案,但可以为不同场景提供清晰的判断依据。
如果需要在已有缓存、全局状态或统计对象上增量写入,并且原始数据可以在原地改变,update方法是最自然的选择。它语法简洁,不需要生成中间对象,执行开销较小。在初始化函数、批量更新配置、清理临时数据或累计统计结果时尤其常用。使用update方法可以让调用者明确意识到当前数据正在被修改。
如果数据来自不可变配置或需要保留历史版本,则应优先考虑返回新字典的|或**。其中,在Python 3.9及以上版本中,|的可读性更强,表达“取并集且右侧优先”的语义非常清晰;在需要兼容更多运行环境的场景下,**更为稳妥。对于只需要合并少量字典且不追求原地修改的情况,**也能方便地加入额外键值。
四、注意事项与实践建议
使用update方法时,最常见的错误是把它当作返回合并结果的函数来赋值。下面示例中,result得到的不是合并后的字典,而是None。正确做法是先调用update,再使用原字典。这个细节在编写链式操作或封装工具函数时需要特别留意。
# 错误用法:试图接收update方法的返回值
state = {'a': 1}
patch = {'b': 2}
result = state.update(patch)
print(result) # 输出:None
print(state) # 输出:{'a': 1, 'b': 2}
三种方式在处理重复键时都遵循右侧优先的规则,即后出现的字典键值对会覆盖先出现的同名键。合并前应明确优先级:如果业务上默认值不应覆盖用户值,需要调整字典的顺序或提前进行深拷贝。例如{**defaults, **user_input}会让user_input覆盖defaults,这与多数场景预期一致;反过来反过来,{**user_input, **defaults} 会让默认值覆盖用户输入,这通常不符合预期,除非业务上确实需要默认值优先。因此,合并前应固定字典顺序,避免因顺序调整引入隐蔽错误。
另一个容易忽略的问题是浅拷贝。三种方式都只复制第一层引用,嵌套字典、列表等可变对象仍会被原字典共享。例如:
config = {'db': {'host': 'localhost'}}
override = {'db': {'port': 5432}}
merged = {**config, **override}
merged['db']['host'] = 'prod'
print(config['db']['host']) # 输出:prod
上面的代码修改合并结果中的嵌套字典后,原 config 中的嵌套字典也发生了变化。如果希望彻底隔离,需要在使用前进行深拷贝,或专门实现递归合并:
import copy
merged = copy.deepcopy({**config, **override})
但深拷贝会带来额外开销,只应在确实存在嵌套可变对象且需要隔离时使用。更细粒度的做法是编写按需递归的 deep_merge 函数:
def deep_merge(base, override):
result = base.copy()
for key, value in override.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
这样可以让内层字典按规则合并,而不是直接替换。需要特别注意,递归合并只适合值类型明确为字典的场景,如果嵌套结构中可能出现列表或自定义对象,应设计更明确的结构或使用专门的配置库。
键类型同样值得留意。Python 字典依据哈希值判断重复键,1 和 True、0 和 False 会被视为同一个键。以下示例会覆盖值:
d1 = {1: 'one'}
d2 = {True: 'true'}
merged = {**d1, **d2}
print(merged) # 输出:{1: 'true'}
合并前应保证键类型一致,避免使用布尔值与整数混用作为键,否则可能得到难以察觉的错误结果。
当需要合并大量字典时,应避免在循环中反复使用 ** 或 | 生成中间字典。更高效的做法是先创建空结果,再逐次原地更新:
merged = {}
for item in dicts:
merged.update(item)
这可以减少不必要的内存分配,尤其适合字典数量较大或体积较大的场景。如果希望保留每个输入字典,则可以使用 functools.reduce 配合 |,但可读性可能下降。
从可读性和兼容性来看,Python 3.9 及以上推荐使用 |,因为它把“合并并返回新字典”的意图表达得最直接;在需要兼容旧版本或快速构造额外键值对时,** 仍然实用;在需要原地更新已有状态时,update 不可替代。三种方式并非竞争关系,而是对应不同约束下的选择。
总之,合并字典前先问自己:原字典是否可以修改、是否需要保留历史版本、是否需要兼容老解释器。明确这三点后再选择 update、| 或 **,并始终注意重复键的覆盖顺序、浅拷贝影响以及键类型的一致性。对于复杂嵌套结构,应额外封装深合并逻辑,而不是寄希望于内建语法一步到位。