跳转至

7. 如何用一行代码合并2个字典

题目

有2个Python字典,想写一行代码能返回2个字典合并后的结果,但是 update() 并不返回合并后的结果,而是就地修改其中的一个字典。

>>> x = {'a':1, 'b': 2}
>>> y = {'b':10, 'c': 11}
>>> z = x.update(y)
>>> print(z)
None
>>> x
{'a': 1, 'b': 10, 'c': 11}

怎样才能在z中得到合并结果,而不是在x

另外,dict.update()方法遇到冲突时保留最后一个键值对也是我所期望的。 链接

回答一

如何用一行代码合并2个字典

对于字典x, y, z是在x中替换并合并了y中的值的字典。

  • Python3.5+
z = {**x, **y}
  • Python2, (或者3.4 或更低版本) 可以写一个函数
def merge_two_dicts(x, y):
    z = x.copy()   # z初始化为x
    z.update(y)    # 用y的值修改z,返回值None
    return z

接着

z = merge_two_dicts(x, y)

解释

你有2个字典,想合并这两个字典到一个新字典中,而不改变原有的字典。

x = {'a': 1, 'b': 2}
y = {'b': 3, 'c': 4}

期望的结果是得到一个合并后新字典,第二个字典的值覆盖第一个字典的值。

>>> z
{'a': 1, 'b': 3, 'c': 4}

有一个新的语法能够达到目的,PEP 448提出,Python 3.5中可用

z = {**x, **y}

这的确是单行表达式,Python 3.5, PEP448的发布计划中可以看到,而且已经写入了 What's New in Python 3.5文档。

然而,很有组织仍在使用Python2,你可能希望有一种向后兼容的方式。Python2,Python3.0-3.4种最Pythonic的方式,是这2步。

z = x.copy()
z.update(y) # z被改变,返回值为None

2种方法中,y出现在后面,所以它的值将会覆盖x的值,因此最终的结果,b将会变成3

没用Python3.5,仍旧想一行搞定

如果你没有使用Python3.5,或者的确想写向后兼容的代码,同时想一行表达式搞定,最高效正确的方式是写一个函数。

def merge_two_dicts(x, y):
    """Given two dicts, merge them into a new dict as a shallow copy."""
    z = x.copy()
    z.update(y)
    return z

然后就可以单行搞定了。

z = merge_two_dicts(x, y)

你也能写一个函数合并不定数量的字典,0到N个字典

def merge_dicts(*dict_args):
    """
    Given any number of dicts, shallow copy and merge into a new dict,
    precedence goes to key value pairs in latter dicts.
    """
    result = {}
    for dictionary in dict_args:
        result.update(dictionary)
    return result

这个函数Python2和Python3种都能运行,例如给定从ag的字典:

z = merge_dicts(a, b, c, d, e, f, g)

g 中的键值对优先级比 af要高,以此类推。

对其他答案的批评

不要使用如下的方式

z = dict(x.items() + y.items())

在Python2中,会在内存中分别为2个字典创建2个列表,然后在内存中创建第3个列表,长度等于前两者之和,然后丢弃这三个列表去创建一个新的字典。在Python3种,这会失败,因为你想合并2个 dict_items,而不是2个列表

>>> c = dict(a.items() + b.items())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'dict_items' and 'dict_items'

你需要显式地把它们转换成列表,例如 z = dict(list(x.items()) + list(y.items())),这是对资源和算力的一种浪费。

相似的,在Python3中取 items()的并集(Python2.7种的viewitems())也会失败当值是一些不可哈希的对象时(例如列表)。及时值都是可哈希的,由于集合是无序的,行为取决于集合元素的优先级,因此不要这么做

>>> c = dict(a.items() | b.items())

这个例子展示了当值是不可hash时会发生什么

>>> x = {'a': []}
>>> y = {'b': []}
>>> dict(x.items() | y.items())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

这有一个例子展示了因集合的无序性,导致y的优先级失效,最终的结果保留了x的值。

>>> x = {'a': 2}
>>> y = {'a': 1}
>>> dict(x.items() | y.items())
{'a': 2}

另一种取巧的方法也不应当使用

z = dict(x, **y)

这种方法使用了字典的构造函数,非常快,内存占用非常小(仅比最开始的2步的方法多一点)。但是除非你准确地知道将会发生什么(那就是,第二个字典将会作为关键字参数传给字典的构造函数),这很难理解,也不是预期的用法,这不Pythonic。

这有一个这种用法在django中被纠正的例子。remediated in django

字典期望接受可哈希的键(例如,不可变集合(frozensets)和元组),但是这个方法当键不是字符串的时候,在Python3中会失败。

>>> c = dict(a, **b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: keyword arguments must be strings

邮件列表中,Python语言的创始人,Guido van Rossum 写道:

我宣布 dict({}, **{1:3})是非法的,因为它滥用了 ** 的机制

另一句

很明显,dict(x, **y) 比 "call x.update(y) and return x"更cool,个人认为比起cool,更despicable(卑鄙)

我的理解(也是语言创始人的理解):dict(**y)的目的是创建字典时增强可读性的,例如

dict(a=1, b=10, c=11)

而不是

{'a': 1, 'b': 10, 'c': 11}

对评论的回复

不管Guido所说,dict(x, **y)符合语法规范,而且在Python2和Python3中都能工作,这只适用于键为字符串的原因是关键字参数的工作机制导致的,而不是字典的一种简写机制。在这个地方,使用 ** 操作符也没有滥用机制,实际上 ** 的设计恰恰是为了将关键字传递给字典。

而且,当键不是字符串时,Python3中不工作。隐式的调用约定是命名空间采用普通的字典,用户必须只传递字符串关键字,其他类型都是违背了这一约定的。在Python2中 dict打破了这个一致性。

>>> foo(**{('a', 'b'): None})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() keywords must be strings
>>> dict(**{('a', 'b'): None})
{('a', 'b'): None}

这种不一致性存在于Python的其他实现(Pypy, Jython, IronPython),在Python3中被修复,因为这可能是个突破性的变化。

向你指出,故意编写只能在一种语言版本中或者只在特定约束条件下才能工作的代码是无能的体现。

另外一个评论

dict(x.items() + y.items())在Python2中仍是可读性最强的写法,可读性更重要。

我的回复:对我而言merge_two_dicts(x, y)可读性更好。如果不是前向兼容的问题,Python2将会加速被废弃。

性能较差但正确的写法

以下写法性能较差,但是也是正确的方式。比起copyupdate方法以及新的展开方法,性能会差跟多,原因是迭代了每一个键值对,但是保证了优先级(后面的字典优先级高)

你也用一个字典推导中手动合并字典。

{k: v for d in dicts for k, v in d.items()} # iteritems in Python 2.7

或者在Python2.6中(或者更早到2.4的版本,生成器表达式被引入):

dict((k, v) for d in dicts for k, v in d.items())

itertools.chain也会通过键值对迭代的方式正确合并。

import itertools
z = dict(itertools.chain(x.iteritems(), y.iteritems()))

性能分析

用以下方法做性能分析

import timeit

运行环境:Ubuntu 14.04

Python 2.7 (system Python):

>>> min(timeit.repeat(lambda: merge_two_dicts(x, y)))
0.5726828575134277
>>> min(timeit.repeat(lambda: {k: v for d in (x, y) for k, v in d.items()} ))
1.163769006729126
>>> min(timeit.repeat(lambda: dict(itertools.chain(x.iteritems(), y.iteritems()))))
1.1614501476287842
>>> min(timeit.repeat(lambda: dict((k, v) for d in (x, y) for k, v in d.items())))
2.2345519065856934

In Python 3.5 (deadsnakes PPA):

>>> min(timeit.repeat(lambda: {**x, **y}))
0.4094954460160807
>>> min(timeit.repeat(lambda: merge_two_dicts(x, y)))
0.7881555100320838
>>> min(timeit.repeat(lambda: {k: v for d in (x, y) for k, v in d.items()} ))
1.4525277839857154
>>> min(timeit.repeat(lambda: dict(itertools.chain(x.items(), y.items()))))
2.3143140770262107
>>> min(timeit.repeat(lambda: dict((k, v) for d in (x, y) for k, v in d.items())))
3.2069112799945287

参考资源