如何在一个表达式中合并两个字典?

我有两个 Python 字典,我想编写一个返回合并的这两个字典的表达式。如果update()方法返回其结果而不是就地修改 dict,则将是我需要的方法。

>>> 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()的最后一个胜利处理dict.update() 。)

答案

如何在一个表达式中合并两个 Python 字典?

对于字典xyz变成浅表合并的字典,其中y值替换x

  • 在 Python 3.5 或更高版本中:

    z = {**x, **y}
  • 在 Python 2(或 3.4 或更低版本)中,编写一个函数:

    def merge_two_dicts(x, y):
        z = x.copy()   # start with x's keys and values
        z.update(y)    # modifies z with y's keys and values & returns None
        return z

    现在:

    z = merge_two_dicts(x, y)

请注意, 这里讨论了一个建议(PEP 584) ,它通过为dict一个合并运算符(期望为+ 来进一步简化此操作,以允许:

z = x + y                       # pseudocode for now...

但这尚未实现。

说明

假设您有两个字典,并且想要将它们合并为新字典而不更改原始字典:

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

理想的结果是获得一个合并了值的新字典( z ),第二个字典的值覆盖第一个字典的值。

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

PEP 448 中提出并从 Python 3.5 开始可用的新语法是

z = {**x, **y}

它确实是一个表达。

注意,我们也可以使用文字符号合并:

z = {**x, 'foo': 1, 'bar': 2, **y}

现在:

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

它现在显示为在3.5发布时间表中实现,PEP 478 ,并且已进入Python 3.5 的新功能文档。

但是,由于许多组织仍在使用 Python 2,因此您可能希望以向后兼容的方式进行操作。在 Python 2 和 Python 3.0-3.4 中可用的经典 Pythonic 方法是分两步完成的:

z = x.copy()
z.update(y) # which returns None since it mutates z

在这两种方法中, y将排在第二位,其值将替换x的值,因此最终结果中的'b'将指向3

尚未在 Python 3.5 上运行,但需要一个表达式

如果您尚未使用 Python 3.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)

您还可以创建一个函数来合并未定义数量的 dict,从零到非常大的数量:

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

此函数将在 Python 2 和 3 中适用于所有字典。例如,给定ag

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

g键值对将优先于字典af ,依此类推。

其他答案的批判

不要使用以前接受的答案中看到的内容:

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

在 Python 2 中,您将在每个内存字典中创建两个列表,在内存中创建第三个列表,其长度等于前两个字典的长度,然后丢弃所有三个列表以创建字典。 在 Python 3 中,这将失败,因为您将两个dict_items对象添加在一起,而不是两个列表 -

>>> 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())) 。这浪费了资源和计算能力。

类似地,当值是不可散列的对象(例如,列表items()在 Python 3 中使用items()的并集viewitems()在 Python 2.7 中使用viewitems() )也会失败。即使您的值是可哈希的, 由于集合在语义上是无序的,因此关于优先级的行为是不确定的。所以不要这样做:

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

此示例演示了值不可散列时会发生的情况:

>>> 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)

这使用了dict构造函数,并且非常快且内存效率高(甚至比我们的两步过程略高),但是除非您确切地知道这里正在发生什么(也就是说,第二个 dict 作为关键字参数传递给了 dict 构造函数),它很难阅读,它不是预期的用法,因此不是 Pythonic。

这是在 django修复的用法示例。

字典旨在获取可散列的键(例如,frozenset 或元组),但是当键不是字符串时此方法在 Python 3 中失败。

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

邮件列表中 ,该语言的创建者 Guido van Rossum 写道:

我宣布 dict({},** {1:3})非法是可以的,因为毕竟这是对 ** 机制的滥用。

显然 dict(x,** y)被 “调用 x.update(y)并返回 x” 的 “酷砍”。我个人觉得它比酷更卑鄙。

根据我的理解(以及对语言创建者的理解), dict(**y)的预期用途是出于可读性目的创建字典,例如:

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

代替

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

对评论的回应

尽管 Guido 说了什么, dict(x, **y)符合 dict 规范,顺便说一句。它仅适用于 Python 2 和 3。事实上,这仅适用于字符串键,这是关键字参数如何工作的直接结果,而不是字典的简称。在这个地方使用 ** 运算符也不会滥用该机制,实际上 ** 正是为了将 dict 作为关键字传递而设计的。

同样,当键为非字符串时,它不适用于 3。隐式调用协定是名称空间采用普通命令,而用户只能传递字符串形式的关键字参数。所有其他可调用对象都强制执行了它。 dict打破了 Python 2 中的这种一致性:

>>> 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),这种不一致是很糟糕的。因此,它在 Python 3 中已得到修复,因为这种用法可能是一个重大更改。

我向您指出,故意编写仅在一种语言版本中有效或仅在特定的任意约束下有效的代码是恶意的无能。

更多评论:

dict(x.items() + y.items())仍然是 Python 2 可读性最高的解决方案。

我的回答:如果我们实际上担心可读性,则merge_two_dicts(x, y)在我看来实际上要清晰得多。而且它不向前兼容,因为 Python 2 越来越不推荐使用。

{**x, **y}似乎不处理嵌套字典。嵌套键的内容只是被覆盖,而不是被合并。我最终被这些没有递归合并的答案所烧死,我很惊讶没有人提到它。在我对 “合并” 一词的解释中,这些答案描述的是 “将一个词典与另一个词典更新”,而不是合并。

是。我必须回头再问这个问题,它要求两个字典进行浅层合并,第一个字典的值被第二个字典的值覆盖 - 在一个表达式中。

假设有两个字典,一个字典可能会递归地将它们合并到一个函数中,但是您应注意不要修改任何一个字典中的字典,避免这种情况的最可靠方法是在分配值时进行复制。由于键必须是可散列的,因此通常是不可变的,因此复制它们毫无意义:

from copy import deepcopy

def dict_of_dicts_merge(x, y):
    z = {}
    overlapping_keys = x.keys() & y.keys()
    for key in overlapping_keys:
        z[key] = dict_of_dicts_merge(x[key], y[key])
    for key in x.keys() - overlapping_keys:
        z[key] = deepcopy(x[key])
    for key in y.keys() - overlapping_keys:
        z[key] = deepcopy(y[key])
    return z

用法:

>>> x = {'a':{1:{}}, 'b': {2:{}}}
>>> y = {'b':{10:{}}, 'c': {11:{}}}
>>> dict_of_dicts_merge(x, y)
{'b': {2: {}, 10: {}}, 'a': {1: {}}, 'c': {11: {}}}

提出其他价值类型的偶然性远远超出了此问题的范围,因此,我将针对 “字典合并词典” 中的规范问题向您指出。

性能较差但临时性正确

这些方法的性能较差,但是它们将提供正确的行为。它们的性能将比copyupdate或新的拆包要差得多,因为它们在更高的抽象级别上遍历每个键值对,但是它们确实遵循优先级的顺序(后者决定优先级)

您还可以在 dict 理解内手动将 dict 链接:

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

或在 python 2.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(系统 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

在 Python 3.5(死神 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

词典资源

就您而言,您可以做的是:

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

可以根据需要将最终的 dict 放入z ,并用第二个( y )dict 的值正确覆盖键b的值:

>>> x = {'a':1, 'b': 2}
>>> y = {'b':10, 'c': 11}
>>> z = dict(x.items() + y.items())
>>> z
{'a': 1, 'c': 11, 'b': 10}

如果您使用 Python 3,它只会稍微复杂一点。要创建z

>>> z = dict(list(x.items()) + list(y.items()))
>>> z
{'a': 1, 'c': 11, 'b': 10}

替代:

z = x.copy()
z.update(y)

另一个更简洁的选择:

z = dict(x, **y)

注意 :这已经成为一个流行的答案,但必须指出的是,如果y具有任何非字符串键,那么这实际上是对 CPython 实现细节的滥用,并且在 Python 3 中不起作用,或者使用 PyPy,IronPython 或 Jython。另外, Guido 也不是粉丝 。因此,我不建议将此技术用于前向兼容或交叉实现的可移植代码,这实际上意味着应完全避免使用它。

这可能不是一个流行的答案,但是您几乎可以肯定不想这样做。如果要合并的副本,请使用 copy(或deepcopy ,具体取决于您的需求),然后进行更新。与使用. items()+ .items()创建单行代码相比,两行代码更具可读性 - 更具 Python 风格。显式胜于隐式。

此外,当您使用. items()(Python 3.0 之前的版本)时,您正在创建一个新列表,其中包含字典中的项目。如果您的词典很大,那将是很多开销(创建合并字典后将立即丢弃两个大列表)。 update()可以更高效地工作,因为它可以逐项执行第二个字典。

时间方面

>>> timeit.Timer("dict(x, **y)", "x = dict(zip(range(1000), range(1000)))\ny=dict(zip(range(1000,2000), range(1000,2000)))").timeit(100000)
15.52571702003479
>>> timeit.Timer("temp = x.copy()\ntemp.update(y)", "x = dict(zip(range(1000), range(1000)))\ny=dict(zip(range(1000,2000), range(1000,2000)))").timeit(100000)
15.694622993469238
>>> timeit.Timer("dict(x.items() + y.items())", "x = dict(zip(range(1000), range(1000)))\ny=dict(zip(range(1000,2000), range(1000,2000)))").timeit(100000)
41.484580039978027

IMO 出于可读性考虑,前两者之间的微小速度下降是值得的。此外,仅在 Python 2.3 中添加了用于字典创建的关键字参数,而 copy()和 update()将在旧版本中工作。

在后续回答中,您询问了这两种选择的相对性能:

z1 = dict(x.items() + y.items())
z2 = dict(x, **y)

至少在我的机器上(运行 Python 2.5.2 的相当普通的 x86_64),替代z2不仅更短,更简单,而且显着更快。您可以使用 Python 随附的timeit模块timeit验证。

示例 1:相同的字典将 20 个连续的整数映射到自身:

% python -m timeit -s 'x=y=dict((i,i) for i in range(20))' 'z1=dict(x.items() + y.items())'
100000 loops, best of 3: 5.67 usec per loop
% python -m timeit -s 'x=y=dict((i,i) for i in range(20))' 'z2=dict(x, **y)' 
100000 loops, best of 3: 1.53 usec per loop

z2胜出 3.5 倍左右。不同的词典似乎会产生完全不同的结果,但是z2似乎总是领先。 (如果同一测试的结果不一致,请尝试将-r传递的数字大于默认值 3。)

示例 2:非重叠字典将 252 个短字符串映射为整数,反之亦然:

% python -m timeit -s 'from htmlentitydefs import codepoint2name as x, name2codepoint as y' 'z1=dict(x.items() + y.items())'
1000 loops, best of 3: 260 usec per loop
% python -m timeit -s 'from htmlentitydefs import codepoint2name as x, name2codepoint as y' 'z2=dict(x, **y)'               
10000 loops, best of 3: 26.9 usec per loop

z2赢了大约 10 倍。这在我的书中是一个很大的胜利!

比较这两个之后,我想知道z1的性能差是否可以归因于构建两个项目列表的开销,这反过来又使我想知道这种变化是否可能更好地起作用:

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

一些快速测试,例如

% python -m timeit -s 'from itertools import chain; from htmlentitydefs import codepoint2name as x, name2codepoint as y' 'z3=dict(chain(x.iteritems(), y.iteritems()))'
10000 loops, best of 3: 66 usec per loop

我得出的结论是z3z1快一些,但不及z2快。绝对不值得所有额外的输入。

讨论中仍然缺少一些重要的内容,这是这些替代方法与合并两个列表的 “明显” 方法的性能比较:使用update方法。为了使事物与表达式保持一致,而不会修改 x 或 y,我将制作 x 的副本,而不是就地对其进行修改,如下所示:

z0 = dict(x)
z0.update(y)

典型结果:

% python -m timeit -s 'from htmlentitydefs import codepoint2name as x, name2codepoint as y' 'z0=dict(x); z0.update(y)'
10000 loops, best of 3: 26.9 usec per loop

换句话说, z0z2似乎具有基本相同的性能。您认为这可能是巧合吗?我不....

实际上,我什至宣称纯 Python 代码不可能做得比这更好。而且,如果您可以在 C 扩展模块中做得更好,我想 Python 人士可能会对将您的代码(或您的方法的变体)并入 Python 核心感兴趣。 Python 在很多地方都使用dict ;优化运营非常重要。

您也可以这样写

z0 = x.copy()
z0.update(y)

就像 Tony 一样,但是(并不奇怪)表示法上的差异对性能没有任何可测量的影响。使用对您而言合适的任何一种。当然,他指出两语句版本更容易理解是绝对正确的。

在 Python 3.0 及更高版本中 ,您可以使用collections.ChainMap将多个字典或其他映射组合在一起,以创建一个可更新的视图:

>>> from collections import ChainMap
>>> x = {'a':1, 'b': 2}
>>> y = {'b':10, 'c': 11}
>>> z = ChainMap({}, y, x)
>>> for k, v in z.items():
        print(k, '-->', v)

a --> 1
b --> 10
c --> 11

适用于 Python 3.5 和更高版本的更新 :可以使用PEP 448扩展词典打包和拆包。快速简便:

>>> x = {'a':1, 'b': 2}
>>> y = y = {'b':10, 'c': 11}
>>> {**x, **y}
{'a': 1, 'b': 10, 'c': 11}

我想要类似的东西,但是能够指定如何合并重复键上的值,所以我破解了这个(但并未对其进行大量测试)。显然,这不是单个表达式,而是单个函数调用。

def merge(d1, d2, merge_fn=lambda x,y:y):
    """
    Merges two dictionaries, non-destructively, combining 
    values on duplicate keys as defined by the optional merge
    function.  The default behavior replaces the values in d1
    with corresponding values in d2.  (There is no other generally
    applicable merge strategy, but often you'll have homogeneous 
    types in your dicts, so specifying a merge technique can be 
    valuable.)

    Examples:

    >>> d1
    {'a': 1, 'c': 3, 'b': 2}
    >>> merge(d1, d1)
    {'a': 1, 'c': 3, 'b': 2}
    >>> merge(d1, d1, lambda x,y: x+y)
    {'a': 2, 'c': 6, 'b': 4}

    """
    result = dict(d1)
    for k,v in d2.iteritems():
        if k in result:
            result[k] = merge_fn(result[k], v)
        else:
            result[k] = v
    return result

递归 / 深度更新字典

def deepupdate(original, update):
    """
    Recursively update a dict.
    Subdict's won't be overwritten but also updated.
    """
    for key, value in original.iteritems(): 
        if key not in update:
            update[key] = value
        elif isinstance(value, dict):
            deepupdate(value, update[key]) 
    return update

示范:

pluto_original = {
    'name': 'Pluto',
    'details': {
        'tail': True,
        'color': 'orange'
    }
}

pluto_update = {
    'name': 'Pluutoo',
    'details': {
        'color': 'blue'
    }
}

print deepupdate(pluto_original, pluto_update)

输出:

{
    'name': 'Pluutoo',
    'details': {
        'color': 'blue',
        'tail': True
    }
}

感谢 rednaw 的编辑。

我不使用副本时可能想到的最佳版本是:

from itertools import chain
x = {'a':1, 'b': 2}
y = {'b':10, 'c': 11}
dict(chain(x.iteritems(), y.iteritems()))

它比dict(x.items() + y.items())快,但不如n = copy(a); n.update(b)n = copy(a); n.update(b) ,至少在 CPython 上。如果将iteritems()更改为items() ,则此版本在 Python 3 中也可以使用,这是 2to3 工具自动完成的。

我个人最喜欢这个版本,因为它用一种功能语法很好地描述了我想要的内容。唯一的小问题是,来自 y 的值优先于来自 x 的值并不完全清楚,但我不认为很难弄清楚。