这篇教程探讨了哪种合并字典的方式才是最符合Python语言习惯的(idiomatic)。首发于微信公众号“编程派”,阅读最新Python教程,请关注编程派。 你有没有想过在Python中合并两个或以上字典? 有很多种方法可以解决这个问题:有些比较拙劣,有些不准确,而且大部分都要许多行代码。 接下来我们一一介绍解决这个问题的不同方法,一起探讨到底哪一种是最Pythonic的。 我们的问题在讨论解决方案之前,我们需要明确定义问题。 我们的代码中有两个字典: user 和 defaults 。我们希望将二者合并至一个叫context 的新字典里。 需要满足以下要求:
注意:对于第五个要求,我们关注的是对字典的更新,而不是其中包含的对象。如果担心字典中嵌套对象的可变性,我们可以考虑使用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。但是这又是非常主观的。下面是我们使用的一些评判标准:
可能的解决方法既然定义完了需要解决的问题,接下来我们探讨下都有哪些解决方法,并分析其中哪个最准确,哪个最符合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中才有效。 得分:
字典解析(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方法。 ChainMapChainMap可以让我们不用遍历初始字典,就创建一个新字典: from collections import ChainMap context = ChainMap({}, user, defaults) ChainMap将多个字典打包成一个proxy对象(一个“视图”);ChainMap查找命令(译者注:如context['name'])会检索其中的字典,直到找到匹配的对象。 这里有几个问题需要回答。
将参数按这样的顺序排列的目的,是为了确保满足第一个点要求。ChainMap是按照顺序检索字典的,所以 user 会在 defaults 之前返回匹配的值。
这是为了满足第五点要求。如果我们修改ChainMap对象,会影响到里面提供的第一个字典。我们不希望 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,建议你一一查看上面介绍的那些方法,确定哪一种最符合你的需求。 (责任编辑:好模板) |