有没有一种Python风格的方法可以合并两个字典(对于在两个字典中都出现的键,将它们的值相加)?

543

举个例子,我有两个字典:

Dict A: {'a': 1, 'b': 2, 'c': 3}
Dict B: {'b': 3, 'c': 4, 'd': 5}
我需要一种Python方式来“合并”两个字典,使结果为:
{'a': 1, 'b': 5, 'c': 7, 'd': 5}
那就是说:如果一个键在两个字典中都出现了,将它们的值相加;如果它只出现在一个字典中,则保留其值。
22个回答

907

使用collections.Counter

>>> from collections import Counter
>>> A = Counter({'a':1, 'b':2, 'c':3})
>>> B = Counter({'b':3, 'c':4, 'd':5})
>>> A + B
Counter({'c': 7, 'b': 5, 'd': 5, 'a': 1})

计数器基本上是 dict 的子类,因此您仍然可以像使用该类型一样对它们执行其他所有操作,例如遍历它们的键和值。


4
如果有多个类似这样需要合并的计数器,应该怎么办?很遗憾,“sum(counters)”不能解决问题。 - Dr. Jan-Philip Gehrcke
30
使用 sum(counters, Counter())sum() 函数设置一个起始值。 - Martijn Pieters
5
谢谢。然而,这种方法是否受到像字符串求和一样的中间对象创建的影响? - Dr. Jan-Philip Gehrcke
6
你的另一选择是使用循环和 += 来进行原地求和。res = counters[0],然后 for c in counters[1:]: res += c - Martijn Pieters
4
我喜欢这种方法!如果有人喜欢将事物保持紧密结合处理字典,可以使用update()代替+=for c in counters[1:]: res.update(c) - Dr. Jan-Philip Gehrcke
显示剩余7条评论

128

一个更通用的解决方案,适用于非数字值:

a = {'a': 'foo', 'b':'bar', 'c': 'baz'}
b = {'a': 'spam', 'c':'ham', 'x': 'blah'}

r = dict(a.items() + b.items() +
    [(k, a[k] + b[k]) for k in set(b) & set(a)])

甚至更通用:

def combine_dicts(a, b, op=operator.add):
    return dict(a.items() + b.items() +
        [(k, op(a[k], b[k])) for k in set(b) & set(a)])
例如:
>>> a = {'a': 2, 'b':3, 'c':4}
>>> b = {'a': 5, 'c':6, 'x':7}

>>> import operator
>>> print combine_dicts(a, b, operator.mul)
{'a': 10, 'x': 7, 'c': 24, 'b': 3}

27
当使用Python 2.7时,您可以使用for k in b.viewkeys() & a.viewkeys(),并跳过创建集合的步骤。这是在这个Counter类中的建议。 - Martijn Pieters
1
@HaiPhan:因为字典是按键而不是键值对进行迭代的。参见list({..})for k in {...}等。 - georg
我知道这很老了,但是最后一行似乎将值相乘而不是像 OP 想要的那样相加... - Craicerjack
2
是的,我使用了operator.mul来表明这段代码是通用的,不仅限于加法。 - georg
10
可以增加一个 Python 3 兼容选项吗?{**a, **b, **{k: op(a[k], b[k]) for k in a.keys() & b}} 应该可以在 Python 3.5+ 上运行。 - vaultah
显示剩余3条评论

84
>>> A = {'a':1, 'b':2, 'c':3}
>>> B = {'b':3, 'c':4, 'd':5}
>>> c = {x: A.get(x, 0) + B.get(x, 0) for x in set(A).union(B)}
>>> print(c)

{'a': 1, 'c': 7, 'b': 5, 'd': 5}

1
使用 for x in set(itertools.chain(A, B)) 不是更合理吗?因为在字典上使用 set 有点无意义,因为键已经是唯一的。我知道这只是另一种获取键集的方法,但我觉得它比使用 itertools.chain 更令人困惑(这意味着您知道 itertools.chain 的作用)。 - jeromej
1
好的回答值得点赞。最好直接将“keys”转换为“set”,跳过“itertools.chain”。为什么要让事情变得更加复杂呢? - ChaimG

48

简介: 以下是(可能)最佳解决方案,但您必须了解并记住它,有时还要希望您的Python版本不太旧或任何可能存在的问题。

然后有一些“hacky”解决方案。它们很棒且简短,但有时很难理解,阅读和记忆。

然而,有一种替代方案,即尝试重新发明轮子。 - 为什么要重新发明轮子? - 通常是因为这是学习的绝佳方式(有时只是因为已经存在的工具不能完全按照您希望的方式执行所需操作),如果您不知道或不记得用于解决问题的完美工具,则是最简单的方法。

因此,我提议重新发明collections模块中Counter类的轮子(至少部分重写):

class MyDict(dict):
    def __add__(self, oth):
        r = self.copy()

        try:
            for key, val in oth.items():
                if key in r:
                    r[key] += val  # You can custom it here
                else:
                    r[key] = val
        except AttributeError:  # In case oth isn't a dict
            return NotImplemented  # The convention when a case isn't handled

        return r

a = MyDict({'a':1, 'b':2, 'c':3})
b = MyDict({'b':3, 'c':4, 'd':5})

print(a+b)  # Output {'a':1, 'b': 5, 'c': 7, 'd': 5}

也许还有其他实现方式,已经有工具可以做到这一点,但是将事情基本上如何运作可视化总是很好的。


3
对于我们仍在使用2.6版本的人来说很好。 - Brian B

18

在这种情况下,肯定将Counter()相加是最符合Python风格的方法,但只有当结果为正值时才行。以下是一个示例,正如您所看到的,在对B字典中的c值取反之后,结果中没有c

In [1]: from collections import Counter

In [2]: A = Counter({'a':1, 'b':2, 'c':3})

In [3]: B = Counter({'b':3, 'c':-4, 'd':5})

In [4]: A + B
Out[4]: Counter({'d': 5, 'b': 5, 'a': 1})

这是因为Counter主要设计用于正整数来表示计数(负计数没有意义)。但为了帮助这些使用情况,Python文档记录了最小范围和类型限制如下:

  • Counter类本身是一个字典子类,对其键和值没有任何限制。值旨在表示计数,但您可以将任何内容存储在值字段中。
  • most_common()方法仅需要值可以排序。
  • 对于c[key] +=1等就地操作,值类型只需支持加法和减法。因此,分数、浮点数和十进制数都可以工作,并且支持负值。对于允许输入和输出的负值和零值的update()subtract()也是如此。
  • 多重集合方法仅设计用于正值的情况。输入可以为负数或零,但仅创建具有正值的输出。没有类型限制,但值类型需要支持加法、减法和比较。
  • elements()方法需要整数计数。它会忽略零和负计数。

因此,在对Counter求和后解决该问题的方法是使用Counter.update以获取所需输出。它的工作方式类似于dict.update(),但是添加计数而不是替换它们。

In [24]: A.update(B)

In [25]: A
Out[25]: Counter({'d': 5, 'b': 5, 'a': 1, 'c': -1})

16
myDict = {}
for k in itertools.chain(A.keys(), B.keys()):
    myDict[k] = A.get(k, 0)+B.get(k, 0)

你可能想要将 itertools.chain(A.keys(), B.keys()) 包装成一个 set(),以避免重复计算相同的键。 - basil_man

13

没有额外的导入!

有一个名为EAFP(宁愿请求原谅,而不是寻求许可)的Python标准。下面的代码是基于这个Python标准。

# The A and B dictionaries
A = {'a': 1, 'b': 2, 'c': 3}
B = {'b': 3, 'c': 4, 'd': 5}

# The final dictionary. Will contain the final outputs.
newdict = {}

# Make sure every key of A and B get into the final dictionary 'newdict'.
newdict.update(A)
newdict.update(B)

# Iterate through each key of A.
for i in A.keys():

    # If same key exist on B, its values from A and B will add together and
    # get included in the final dictionary 'newdict'.
    try:
        addition = A[i] + B[i]
        newdict[i] = addition

    # If current key does not exist in dictionary B, it will give a KeyError,
    # catch it and continue looping.
    except KeyError:
        continue

编辑:感谢jerzyk提供的改进建议。


5
n²算法的速度显著慢于计数器方法。 - Joop
@DeveshSaini 进步了,但仍然不够优化 :) 例如:你真的需要排序吗?然后,为什么要两个循环?你已经在newdict中拥有所有的键,只需要一些小提示来优化。 - Jerzyk
已经使用n^1算法替换了之前的n^2算法 @Joop - Devesh Saini

12
import itertools
import collections

dictA = {'a':1, 'b':2, 'c':3}
dictB = {'b':3, 'c':4, 'd':5}

new_dict = collections.defaultdict(int)
# use dict.items() instead of dict.iteritems() for Python3
for k, v in itertools.chain(dictA.iteritems(), dictB.iteritems()):
    new_dict[k] += v

print dict(new_dict)

# OUTPUT
{'a': 1, 'c': 7, 'b': 5, 'd': 5}

或者,如@Martijn所提到的那样,您可以使用计数器。


8

如果您需要更通用且可扩展的方式,请查看mergedict。它使用singledispatch,并且可以根据其类型合并值。

示例:

from mergedict import MergeDict

class SumDict(MergeDict):
    @MergeDict.dispatch(int)
    def merge_int(this, other):
        return this + other

d2 = SumDict({'a': 1, 'b': 'one'})
d2.merge({'a':2, 'b': 'two'})

assert d2 == {'a': 3, 'b': 'two'}

7

从python 3.5开始:合并和求和

感谢@tokeinizer_fsj在评论中告诉我,我没有完全理解问题的意思(我认为“add”只是将两个字典中可能不同的键添加起来,而实际上我应该对相同键值进行求和)。所以我在合并之前添加了循环,这样第二个字典包含了相同键的总和。最后一个字典将是新字典中持续存在的值,而它们是由两个字典合并而成的结果,因此我认为问题得到了解决。这个解决方案在python 3.5及以上版本中有效。

a = {
    "a": 1,
    "b": 2,
    "c": 3
}

b = {
    "a": 2,
    "b": 3,
    "d": 5
}

# Python 3.5

for key in b:
    if key in a:
        b[key] = b[key] + a[key]

c = {**a, **b}
print(c)

>>> c
{'a': 3, 'b': 5, 'c': 3, 'd': 5}

可重用的代码

a = {'a': 1, 'b': 2, 'c': 3}
b = {'b': 3, 'c': 4, 'd': 5}


def mergsum(a, b):
    for k in b:
        if k in a:
            b[k] = b[k] + a[k]
    c = {**a, **b}
    return c


print(mergsum(a, b))

这种合并字典的方式不会为相同的键添加值。在这个问题中,键b的期望值是5(2+3),但你的方法返回了3 - tokenizer_fsj

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接