Python:优雅地合并字典并对值求和

39

我正在尝试合并来自多个服务器的日志。每个日志都是一个元组列表(date, count)。date可能会出现多次,我希望最终生成的字典将保存来自所有服务器的所有计数的总和。

这是我的尝试,附带一些示例数据:

from collections import defaultdict

a=[("13.5",100)]
b=[("14.5",100), ("15.5", 100)]
c=[("15.5",100), ("16.5", 100)]
input=[a,b,c]

output=defaultdict(int)
for d in input:
        for item in d:
           output[item[0]]+=item[1]
print dict(output)

这将会得到:

{'14.5': 100, '16.5': 100, '13.5': 100, '15.5': 200}

果然如此。

我即将因为一位同事看到了这段代码而疯狂。她坚持认为必须有一种更加Pythonic和优雅的方式来完成,而不需要这些嵌套的for循环。你们有什么想法吗?


2
@AshwiniChaudhary:Counter() 只计算出现次数,而且由于值已经预先填充,它在这种情况下不起作用。 - Christian Witts
@ChristianWitts 请看下面的解决方案。 - Ashwini Chaudhary
8
我认为不太符合 Python 风格的是花时间去担心如何让已经很清晰易懂的代码更符合 Python 风格。需要花费几个小时思考的 Python 风格并不是真正的 Python 风格。 - DSM
@AshwiniChaudhary:每天都有新的学习 :) - Christian Witts
不必要的一行代码(值得抱怨,与你的代码不同):dict([(k, sum([v2 for k2,v2 in a+b+c if k2==k])) for k,v in a+b+c]) - krethika
4个回答

46

我认为没有比这更简单的了:

a=[("13.5",100)]
b=[("14.5",100), ("15.5", 100)]
c=[("15.5",100), ("16.5", 100)]
input=[a,b,c]

from collections import Counter

print sum(
    (Counter(dict(x)) for x in input),
    Counter())

请注意,Counter(也被称为多重集)是您的数据最自然的数据结构(一种允许元素出现多次的集合类型,或者等同于具有元素->发生计数语义的映射)。您本可以在第一时间使用它,而不是使用元组列表。


还有其他可能的选择:

from collections import Counter
from operator import add

print reduce(add, (Counter(dict(x)) for x in input))
使用reduce(add, seq)代替sum(seq, initialValue)通常更加灵活,可以避免传递冗余的初始值。
请注意,您还可以使用operator.and_来查找多重集的交集,而不是求和。
由于每个步骤都会创建一个新的Counter对象,因此上述变体非常慢。让我们修复它。
我们知道Counter + Counter返回一个具有合并数据的新Counter。这没问题,但我们想避免额外的创建。让我们改用Counter.update

update(self, iterable=None, **kwds)未绑定的collections.Counter方法

与 dict.update() 类似,但添加计数而不是替换计数。 源可以是可迭代对象、字典或另一个 Counter 实例。

这正是我们想要的。让我们将其包装为与reduce兼容的函数,看看会发生什么。
def updateInPlace(a,b):
    a.update(b)
    return a

print reduce(updateInPlace, (Counter(dict(x)) for x in input))

这只比原始解决方案略慢。

基准测试: http://ideone.com/7IzSx (感谢astynax提供了另一个解决方案)

(另外:如果你非常想要一个一行代码的解决方案,你可以将updateInPlace替换为lambda x,y: x.update(y) or x,它的功能相同,甚至能够证明更快,但可读性不如前者。不要这样做 :-))


1
时间复杂度怎么样?它比 OP 的代码更高效吗? - jerrymouse
我不这么认为。楼主的代码没有创建任何立即对象,因此通常应该更有效率。 - Kos
这种方法确实是在这里呈现的最慢的,OP的原始解决方案是最快的。没有什么意外的。http://ideone.com/HAmvi - Kos
采用更高效的方法进行了修复。基准测试:http://ideone.com/2eGQi - Kos
+1 这真是非常优雅。感谢您提供这个数据结构 - 它像手套一样适合这个问题;而且由于数据量不足,性能也无需担心。 - Adam Matan
1
@Kos,我稍微修改了基准测试。最快的方法是:defaultdict + chain(以及我在中间重写的merge_with) :) - Aleksei astynax Pirogov

11
from collections import Counter


a = [("13.5",100)]
b = [("14.5",100), ("15.5", 100)]
c = [("15.5",100), ("16.5", 100)]

inp = [dict(x) for x in (a,b,c)]
count = Counter()
for y in inp:
  count += Counter(y)
print(count)

输出:

Counter({'15.5': 200, '14.5': 100, '16.5': 100, '13.5': 100})

编辑: 正如duncan所建议,你可以使用一行代码替换这3行:

   count = Counter()
    for y in inp:
      count += Counter(y)

替换为:count = sum((Counter(y) for y in inp), Counter())


3
你甚至可以使用sum函数来替代for循环:count = sum((Counter(y) for y in inp), Counter()) - Duncan
@Duncan 谢谢,我之前不知道这个,已经实现了你的建议。 - Ashwini Chaudhary
如果它们已经是计数器,那么只需使用count = sum(inp, Counter())。 :D - endolith

7
你可以使用itertools的groupby方法来实现:

groupby

from itertools import groupby, chain

a=[("13.5",100)]
b=[("14.5",100), ("15.5", 100)]
c=[("15.5",100), ("16.5", 100)]
input = sorted(chain(a,b,c), key=lambda x: x[0])

output = {}
for k, g in groupby(input, key=lambda x: x[0]):
  output[k] = sum(x[1] for x in g)

print output

使用groupby而不是两个循环和defaultdict,可以使您的代码更清晰。

2
你可以使用operator.itemgetter(0)代替lambda :) - Kos
1
错误:如您所提到的文档中所述,“groupby”需要先进行排序!这里之所以有效,是因为在“chain(a,b,c)”中,“b[1]”和“c[0]”将是连续的,但如果您改为使用“chain(a,c,b)”,则结果将不正确(对于“output['15.5']”,您将得到100而不是200)。 - Emmanuel
1
我想这是个人口味问题,但我觉得这比defaultdict难读,并且比起OP方法更慢。 - fraxel
不客气,无论如何都很高兴你考虑了 groupby - Emmanuel
哈哈,性能分析赢了。我说它不够优化,但仍然比我的快。 :) - Kos
显示剩余2条评论

2
你可以使用 Counter 或 defaultdict,或者你可以尝试我的变体:
def merge_with(d1, d2, fn=lambda x, y: x + y):
    res = d1.copy() # "= dict(d1)" for lists of tuples
    for key, val in d2.items(): # ".. in d2" for lists of tuples
        try:
            res[key] = fn(res[key], val)
        except KeyError:
            res[key] = val
    return res

>>> merge_with({'a':1, 'b':2}, {'a':3, 'c':4})
{'a': 4, 'c': 4, 'b': 2}

甚至更加通用:

def make_merger(fappend=lambda x, y: x + y, fempty=lambda x: x):
    def inner(*dicts):
        res = dict((k, fempty(v)) for k, v
            in dicts[0].items()) # ".. in dicts[0]" for lists of tuples
        for dic in dicts[1:]:
            for key, val in dic.items(): # ".. in dic" for lists of tuples
                try:
                    res[key] = fappend(res[key], val)
                except KeyError:
                    res[key] = fempty(val)
        return res
    return inner

>>> make_merger()({'a':1, 'b':2}, {'a':3, 'c':4})
{'a': 4, 'c': 4, 'b': 2}

>>> appender = make_merger(lambda x, y: x + [y], lambda x: [x])
>>> appender({'a':1, 'b':2}, {'a':3, 'c':4}, {'b':'BBB', 'c':'CCC'})
{'a': [1, 3], 'c': [4, 'CCC'], 'b': [2, 'BBB']}

另外,你可以继承 dict 类并实现一个 __add__ 方法:


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