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

2022-09-05 00:42:45

我想将两个字典合并到一个新字典中。

x = {'a': 1, 'b': 2}
y = {'b': 3, 'c': 4}
z = merge(x, y)

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

每当两个字典中都存在一个键时,就只应保留该值。ky[k]


答案 1

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

对于字典 和 ,它们的浅合并字典从 中获取值,从 中替换值。xyzyx

  • 在 Python 3.9.0 或更高版本(发布于 2020 年 10 月 17 日,PEP-584此处讨论)中

    z = x | y
    
  • 在 Python 3.5 或更高版本中:

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

    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
    

    现在:

    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

在这两种方法中,将排在第二位,其值将取代 的值,因此将指向我们的最终结果。yxb3

尚未在 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) 

中的键值对将优先于字典 ,依此类推。gaf

对其他答案的批评

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

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

类似地,在 Python 3(在 Python 2.7 中)中采用 的并集也会失败,当值是不可哈希的对象(例如列表)时。即使您的值是可哈希的,由于集合在语义上是无序的,因此在优先级方面行为是未定义的。所以不要这样做:items()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'

下面是一个应具有优先级的示例,但由于集合的任意顺序,将保留 from 的值:yx

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

另一个你不应该使用的技巧:

z = dict(x, **y)

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

下面是在 django 中修正用法的示例。

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

>>> 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) 是 “cool hack” for “call x.update(y) 并返回 x” 的“酷黑客”。就个人而言,我发现它比酷更卑鄙。

我的理解(以及语言创建者的理解)是,用于创建用于可读性的字典,例如:dict(**y)

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

而不是

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

对评论的答复

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

同样,当键不是字符串时,它不适用于3。隐式调用协定是命名空间采用普通字典,而用户只能传递作为字符串的关键字参数。所有其他可调用对象都强制执行它。 在 Python 2 中打破了这种一致性: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),这种不一致是很糟糕的。因此,它在Python 3中被修复了,因为这种用法可能是一个重大的变化。

我向你提出,故意编写只能在一种语言的一个版本上工作或仅在给定某些任意约束的情况下工作的代码是恶意的无能。

更多评论:

dict(x.items() + y.items())仍然是Python 2上可读性最强的解决方案。可读性至关重要。

我的回答是:实际上,如果我们真的关心可读性,对我来说似乎要清楚得多。而且它不是向前兼容的,因为Python 2越来越不被弃用。merge_two_dicts(x, y)

{**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: {}}}

为其他值类型提出意外情况远远超出了这个问题的范围,因此我将向您指出我对“字典合并字典”的规范问题的答案

性能较低但正确的临时

这些方法的性能较差,但它们将提供正确的行为。它们的性能将远低于 and 或新的解包,因为它们在更高的抽象级别循环访问每个键值对,但它们确实尊重优先级顺序(后面的字典具有优先级)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

词典资源


答案 2

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

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

这将根据需要,将最终的 dict 放入 ,并使 key 的值被第二个 () dict 的值正确覆盖:zby

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

如果您使用Python 2,您甚至可以删除调用。要创建 z:list()

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

如果您使用Python版本3.9.0a4或更高版本,则可以直接使用:

x = {'a':1, 'b': 2}
y = {'b':10, 'c': 11}
z = x | y
print(z)
{'a': 1, 'c': 11, 'b': 10}