题 如何在单个表达式中合并两个词典?


我有两个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() 也是我正在寻找的。)


3222
2017-09-02 07:44


起源




答案:


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

对于词典 x 和 yz 成为一个合并的字典与值 y 取代那些 x

  • 在Python 3.5或更高版本中,:

    z = {**x, **y}
    w = {'foo': 'bar', 'baz': 'qux', **y}  # merge a dict with literal values
    
  • 在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)
    

说明

假设您有两个dicts,并且您希望将它们合并到一个新的dict而不更改原始的dicts:

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

想要的结果是获得一本新词典(z)合并的值,第二个dict的值覆盖第一个。

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

一个新的语法,提出来了 PEP 448 和 从Python 3.5开始提供是的

z = {**x, **y}

它确实是一个表达式。它现在显示为已实现 发布时间表为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)

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

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

对于所有dicts,此函数将在Python 2和3中使用。例如鉴于 a 至 g

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

和键值对 g 将优先于dicts a 至 f, 等等。

批评其他答案

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

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

在Python 2中,您在内存中为每个dict创建两个列表,在内存中创建第三个列表,其长度等于放在一起的前两个列表的长度,然后丢弃所有三个列表以创建dict。 在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中(viewitems() 在Python 2.7)中,当值是不可用的对象(例如列表)时,它也会失败。即使你的价值观是可以洗的, 由于集合在语义上是无序的,因此在优先级方面的行为是未定义的。所以不要这样做:

>>> 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修复

Dicts旨在采用可清洗密钥(例如frozensets或tuples),但是 当键不是字符串时,此方法在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短路的直接结果。在这个地方也没有使用**运算符滥用该机制,事实上**的设计恰恰是为了将dicts作为关键字传递。

同样,当键是非字符串时,它不适用于3。隐式调用契约是命名空间采用普通的dicts,而用户只能传递字符串的关键字参数。所有其他callables强制执行它。 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越来越被弃用。

性能较差但正确的Ad-hoc

这些方法性能较差,但它们会提供正确的行为。 他们会 少得多 表现高于 copy 和 update 或者新的解包,因为它们在更高的抽象级别迭代每个键值对,但是它们  尊重优先顺序(后面的dicts优先)

你也可以在dict理解中手动链接dicts:

{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(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

字典资源


3352
2017-11-10 22:11





在您的情况下,您可以做的是:

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

这将根据你的需要,将最终的词典放入 z,并为key创建值 b 被第二次正确覆盖(y)dict的价值:

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

1440
2017-09-02 07:50





替代:

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

547
2017-09-02 13:00



澄清为什么这不符合问题提供的标准:它不是单个表达式而且它不返回z。 - Alex
@Alex有时原来的问题不是正确的问题。例如,如果你需要使用正则表达式的非pythonic怪物,可以在一行或两行中执行某些操作。要做X,然后使用两行。该 {**x, **y} 接受的答案中的方法是怪异的。 - neuronet
@neuronet每个oneliner通常只是移动必须发生在不同组件中的代码并在那里解决它。这绝对是其中一个案例。但是其他语言的构造比python更好。并且有一个返回它的元素的引用透明变体是一个很好的东西。 - Alex
这样说吧:如果你需要将两行注释解释给你的人,那么你就可以将代码交给人了......你真的在一行中完成了吗? :)我完全同意Python对此不好:应该有一个更简单的方法。虽然这个答案更加抒情,但它真的是明确的还是明确的? Update 不是人们倾向于使用很多的“核心”功能之一。 - neuronet


另一个更简洁的选择:

z = dict(x, **y)

注意:这已成为一个受欢迎的答案,但重要的是要指出,如果 y 有任何非字符串键,事实上,它的工作原理是滥用CPython实现细节,它在Python 3或PyPy,IronPython或Jython中不起作用。也, Guido不是粉丝。所以我不推荐这种技术用于前向兼容或交叉实现的可移植代码,这实际上意味着它应该完全避免。


273
2017-09-02 15:52





这可能不是一个流行的答案,但你几乎肯定不想这样做。如果你想要一个合并的副本,那么使用副本(或 deepcopy的,取决于你想要的,然后更新。这两行代码比使用.items()+ .items()的单行创建更具可读性 - 更多Pythonic。显式优于隐式。

此外,当您使用.items()(Python 3.0之前)时,您正在创建一个包含dict项目的新列表。如果你的词典很大,那么开销很大(两个大型列表一旦创建合并的dict就会被丢弃)。 update()可以更有效地工作,因为它可以逐项运行第二个dict。

就......而言 时间

>>> 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()将在旧版本中使用。


168
2017-09-08 11:16





在后续回答中,您询问了这两种备选方案的相对表现:

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

在我的机器上,至少(一个相当普通的x86_64运行Python 2.5.2),替代 z2 不仅更短更简单,而且速度更快。您可以使用自己验证这一点 timeit Python附带的模块。

示例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

让我得出结论 z3 有点快 z1,但不是那么快 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

换一种说法, z0 和 z2 似乎具有基本相同的性能。你认为这可能是巧合吗?我不....

事实上,我甚至声称纯Python代码不可能比这更好。如果你能在C扩展模块中做得更好,我想Python人员可能会有兴趣将你的代码(或你的方法的变体)合并到Python核心中。 Python使用 dict 在很多地方;优化其运营是一件大事。

你也可以这样写

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

正如Tony所做的那样,但(并不奇怪)记谱法的差异原来并没有对性能产生任何可衡量的影响。使用适合您的任何一种。当然,他绝对正确地指出双语句版本更容易理解。


116
2017-10-23 02:38



这在Python 3中不起作用; items() 不是可以接合的,而且 iteritems 不存在。 - Antti Haapala


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

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

86
2017-09-04 19:08





在Python 3中,您可以使用 collections.ChainMap 它将多个dicts或其他映射组合在一起以创建单个可更新视图:

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

73
2018-04-28 03:15



但是在使用ChainMap时应该小心谨慎,如果你有重复的密钥,第一个映射的值会被使用,当你调用一个 del 比方说,ChainMap c将删除该密钥的第一个映射。 - Prerit
@Prerit你还期望它做什么?这是链接命名空间的正常工作方式。考虑$ PATH如何在bash中工作。删除路径上的可执行文件并不排除在上游具有相同名称的另一个可执行文件。 - Raymond Hettinger
@Raymond Hettinger我同意,只是加了一个警告。大多数人可能不知道它。 :d - Prerit
我来这里是为了推荐简单的 collections.ChainMap。 - hughdbrown
实际上,即使不使用,也可以实现这里的输出 ChainMap 对?我的意思是,你所要做的只是更新 z = {**x, **y} 按照传统步骤,就是这样! - Steffi Keran Rani J


递归/深度更新字典

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的编辑。


61
2017-11-29 11:52