你有没有想过在Python中合并两个或以上字典?
有很多种方法可以解决这个问题:有些比较拙劣,有些不准确,而且大部分都要许多行代码。
接下来我们一一介绍解决这个问题的不同方法,一起探讨到底哪一种是最Pythonic的。
我们的问题
在讨论解决方案之前,我们需要明确定义问题。
user
和defaults
。我们希望将二者合并至一个叫context
的新字典里。
需要满足以下要求:
- 如果存在重复的键,
user
- 字典中的值应覆盖
defaults
- 字典中的值;
defaults
- 和
user
- 中的键可以是任意合法的键;
defaults
- 和
user
- 中的值可以是任意值;
- 在创建
context
- 字典时,
defaults
- 和
user
- 的元素不能出现变化;
- 更新
context
- 字典时,不能更改
defaults
- 或
user
- 字典。
注意:对于第五个要求,我们关注的是对字典的更新,而不是其中包含的对象。如果担心字典中嵌套对象的可变性,我们可以考虑使用copy.deepcopy。
基本上,我们希望实现下面的操作:
>>> user = {'name': "Trey", 'website': "http://treyhunner.com"}
>>> defaults = {'name': "Anonymous User", 'page_name': "Profile Page"}
>>> context = merge_dicts(defaults, user) # magical merge function
>>> context
{'website': 'http://treyhunner.com', 'name': 'Trey', 'page_name': 'Profile Page'}
我们还要考虑解决方法是否Pythonic。但是这又是非常主观的。下面是我们使用的一些评判标准:
- 解决方法应该简洁,但不简短;
- 解决方法应该可读,但不过度冗长;
- 可能的话,解决方法应该为一行代码,需要的话可以内联化(written inline);
- 解决方法的效率不应该太低。
可能的解决方法
既然定义完了需要解决的问题,接下来我们探讨下都有哪些解决方法,并分析其中哪个最准确,哪个最符合Python语言习惯。
多次更新(multiple_update)
下面是一种最简单的合并字典的方式:
context = {}
context.update(defaults)
context.update(user)
update
方法从其他字典中添加元素。请注意,我们首先添加的是defaults
字典中的元素,以保证user
字典中的重复键会覆盖掉defaults
中的键。
它满足了全部5个要求,所以这个方法是准确的。它总共有3行代码,不能内联执行,但是代码很清晰。
得分:
- 准确:是。
- 符合语言习惯:比较符合,如果能够内联执行的话就更好了
复制,然后更新(copy and update)
defaults
字典,然后使用user
来更新复制的字典。
context = defaults.copy()
context.update(user)
这种方法与前一种区别不大。
defaults
字典的方法,可以很明显地看出defaults
字典代表了默认值。
得分:
- 准确:是。
- 符合语言习惯:是。
字典构造器
dict()
),这样也能复制字典。
context = dict(defaults)
context.update(user)
此法与前一种非常相似, 但是没有前一种直接明了(less explicit)。
得分:
- 准确:是。
- 符合语言习惯:一定程度上符合,不过我更喜欢前两种方案。
关键词参数hack(keywords hack)
你以前可能见过下面这个巧妙的解决方法:
context = dict(defaults, **user)
只有一行代码,看上去很酷嘛。不过,这种解决方法有点难理解。
除了可读性之外,还有一个更严重的问题:这种方案是错的。
字典的键必须是字符串。在Python 2(解释器是CPython)中,我们可以使用非字符串作为键,但别被蒙骗了:这种hack只是凑巧在使用标准CPython运行环境的Python 2中才有效。
得分:
- 准确:否。没有满足第二点要求(键必须有效)
- 符合语言习惯:否。这是一个hack。
字典解析(Dictionary comprehension)
我们尝试下使用字典解析式来解决这个问题:
context = {k: v for d in [defaults, user] for k, v in d.items()}
成功了,但是可读性有点差。
如果我们要处理未知数量的字典,这可能是种好方法,但是我们应该会想把字典解析式拆成多行,提高可读性。在只处理两个字典的情况下,这个双嵌套(double nested)的解析式有点大材小用了。
得分:
- 准确:是。
- 符合语言习惯:可以认为不符合。
元素拼接(concatenate items)
假如我们从每个字典中获取一个元素列表,将列表拼接起来,然后再利用拼接的列表在构建新字典?
context = dict(list(defaults.items()) + list(user.items()))
user
字典中的键值会覆盖掉defaults
字典中的值,因为user
字典的元素位于拼接列表的尾部。
在Python 2下,我们不需要先将字典转换成列表,但是本文中我们使用的是Python 3(你也用的是Python 3,对吧?)。
得分:
- 准确:是。
- 符合语言习惯:不特别符合,代码有些重复。
元素并集(union items)
在Python 3中,字典的items方法会返回一个dict_items对象,这是一个奇怪对象,居然支持并集操作。
context = dict(defaults.items() | user.items())
这种方案挺有意思。可惜并不准确。
user
字典应该覆盖defaults
)。因为两个dict_items对象的并集是一个键值对(key-value pairs)的集合,而集合是无序的,所以重复键的处理方法无法预测。
另外,没有满足第三点要求(可以是任意的值),因为集合要求其中元素必须可哈希的,所以键-值元组中的键和值都必须是可哈希的才行。
得分:
- 准确:否。没有满足第一点和第三点要求。
- 符合语言习惯:否。
Chain items
目前为止,我们讨论的解决方案中,最符合Python语言习惯而且又只有一行代码的实现,是创建两个items的列表,然后拼接并组成新字典。
itertools.chain
来简化items拼接的过程:
from itertools import chain
context = dict(chain(defaults.items(), user.items()))
这种方案效果不错,可能比另外创建两个不必要的列表更加高效。
得分:
准确:是。 符合语言习惯:比较符合,但是有点重复调用items方法。
ChainMap
ChainMap可以让我们不用遍历初始字典,就创建一个新字典:
from collections import ChainMap
context = ChainMap({}, user, defaults)
ChainMap将多个字典打包成一个proxy对象(一个“视图”);ChainMap查找命令(译者注:如context['name'])会检索其中的字典,直到找到匹配的对象。
这里有几个问题需要回答。
- 我们为什么把
user
- 放在
defaults
- 前面?
user
会在defaults
之前返回匹配的值。
- 为什么
user
- 之前有一个空字典?
user
发生变化,所以在前面放了一个空字典。
- 这样真的会返回一个字典吗?
ChainMap对象不是字典,而是类似字典的映射。如果我们的代码中使用鸭子类型(duck typing),使用ChainMap是没问题的,但是需要具体查看ChainMap的特性才能确定。此外,ChainMap对象与其底层的字典是相互勾连的,而且其删除元素的方式也很有趣。
得分:
- 准确:可能准确,需要考虑具体的用例。
- 符合语言习惯:如果我们认为这种实现符合用例,那就是符合习惯的。
ChainMap转换成字典(dict from ChainMap)
如果我们特别想要字典,可以将ChainMap转换成字典:
context = dict(ChainMap(user, defaults))
user
一般出现在defaults
之后;但是在这里却相反。除了这点外,上面的代码还是比较简单,也明显符合我们的要求。
得分:
- 准确:是。
- 符合语言习惯:是。
字典拼接(Dictionary concatenation)
我们能不能把两个字典拼接起来呢?
context = defaults + user
这个想法很好,但可惜却是不合法的。
得分:
- 准确:否。无法执行。
- 符合语言习惯:否。
字典拆分(Dictionary unpacking)
如果你在用Python 3.5,你可以使用一种全新的合并字典的方式(对亏了PEP 448):
context = {**defaults, **user}
这行代码很简洁,很Pythonic。里面有一些特殊符号,但是很明显最后的结果至少是一个字典。
defaults
和user
的元素。它满足我们所有的要求,而且很可能是最简单的一个解决方案。
得分:
- 准确:是。
- 符合语言习惯:是。
小结
在Python中有许多种合并字典的方法,但是能用一行代码优雅地实现的方法并不多。
如果你使用Python 3.5,那么你应该这样解决合并字典的问题:
context = {**defaults, **user}
如果你还没有使用Python 3.5,建议你一一查看上面介绍的那些方法,确定哪一种最符合你的需求。
作者:Trey Hunner 译者:EarlGrey
各种方案的性能比较如下:
multiple_update: 57 ms copy_and_update: 46 ms dict_constructor: 56 ms kwargs_hack: 45 ms dict_comprehension: 45 ms concatenate_items: 166 ms union_items: 163 ms chain_items: 122 ms chainmap: 86 ms dict_from_chainmap: 445 ms dict_unpacking: 27 ms