一尘不染

如何在一个表达式中合并两个字典(取字典的并集)?

python

我有两个 Python 字典,我想编写一个返回这两个字典的单个表达式,合并(即合并)。该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()也是我正在寻找的。)


阅读 217

收藏
2022-01-04

共1个答案

一尘不染

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

对于字典xand yz变成一个浅层合并的字典,其中的值y替换了来自 的值x

  • 在Python 3.9.0或更高(释放2020年10月17日):PEP-584在这里讨论,被实现,并且提供了最简单的方法:

py z = x | y # NOTE: 3.9+ ONLY

  • 在 Python 3.5 或更高版本中:

py z = {**x, **y}

  • 在 Python 2(或 3.4 或更低版本)中编写一个函数:

py def merge_two_dicts(x, y): z = x.copy() # start with keys and values of x z.update(y) # modifies z with keys and values of y return z

现在:

py z = merge_two_dicts(x, y)

解释

假设您有两个字典,并且您想将它们合并到一个新字典中而不改变原始字典:

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 dictionaries, merge them into a new dict as a shallow copy."""
    z = x.copy()
    z.update(y)
    return z

然后你有一个表达式:

z = merge_two_dicts(x, y)

您还可以创建一个函数来合并任意数量的字典,从零到非常大的数字:

def merge_dicts(*dict_args):
    """
    Given any number of dictionaries, shallow copy and merge into a new dict,
    precedence goes to key-value pairs in latter dictionaries.
    """
    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将优先于字典ato 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由于集合的任意顺序而保留了 from 的值:

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

你不应该使用的另一个黑客:

z = dict(x, **y)

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

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

字典旨在采用可散列的键(例如frozensets 或元组),但是当键不是字符串时此方法在 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) 正在作为“call x.update(y) and return x”的“cool hack”四处走动。就我个人而言,我觉得它比酷更卑鄙。

根据我的理解(以及语言创建者的理解),预期用途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: {}}}

提出其他值类型的意外事件远远超出了这个问题的范围。

性能较差但正确的 Ad-hoc

这些方法的性能较低,但它们会提供正确的行为。他们将少得多比高性能copyupdate或新的拆包,因为他们通过在更高的抽象水平的每个键-值对迭代,但他们做的尊重优先顺序(后者字典具有优先权)

您还可以在字典理解中手动链接字典:

{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()) # iteritems in Python 2

itertools.chain 将以正确的顺序将迭代器链接到键值对上:

from itertools import chain
z = dict(chain(x.items(), y.items())) # iteritems in Python 2

性能分析

我只会对已知行为正确的用法进行性能分析。(自包含,因此您可以自己复制和粘贴。)

from timeit import repeat
from itertools import chain

x = dict.fromkeys('abcdefg')
y = dict.fromkeys('efghijk')

def merge_two_dicts(x, y):
    z = x.copy()
    z.update(y)
    return z

min(repeat(lambda: {**x, **y}))
min(repeat(lambda: merge_two_dicts(x, y)))
min(repeat(lambda: {k: v for d in (x, y) for k, v in d.items()}))
min(repeat(lambda: dict(chain(x.items(), y.items()))))
min(repeat(lambda: dict(item for d in (x, y) for item in d.items())))

在 Python 3.8.1 中,NixOS:

>>> min(repeat(lambda: {**x, **y}))
1.0804965235292912
>>> min(repeat(lambda: merge_two_dicts(x, y)))
1.636518670246005
>>> min(repeat(lambda: {k: v for d in (x, y) for k, v in d.items()}))
3.1779992282390594
>>> min(repeat(lambda: dict(chain(x.items(), y.items()))))
2.740647904574871
>>> min(repeat(lambda: dict(item for d in (x, y) for item in d.items())))
4.266070580109954
$ uname -a
Linux nixos 4.19.113 #1-NixOS SMP Wed Mar 25 07:06:15 UTC 2020 x86_64 GNU/Linux
2022-01-04