【问题标题】:Python: Elegantly merge dictionaries with sum() of values [duplicate]Python:优雅地将字典与值的 sum() 合并
【发布时间】:2012-07-02 15:44:37
【问题描述】:

我正在尝试合并来自多台服务器的日志。每个日志都是一个元组列表(datecount)。 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 循环。有什么想法吗?

【问题讨论】:

  • @AshwiniChaudhary:Counter() 只计算出现次数,并且由于这些值已经预先填充,因此不适用于这种情况。
  • @ChristianWitts 在下面查看我的解决方案。
  • 对我来说似乎不符合 Python 标准的是花时间担心让完全清晰的代码更符合 Python 风格。需要数小时思考的 Python 风格并不是真正的 Python 风格。
  • @AshwiniChaudhary:你每天都能学到新东西 :)
  • 不必要的 1-liner(值得投诉,与您的代码不同):dict([(k, sum([v2 for k2,v2 in a+b+c if k2==k])) for k,v in a+b+c])

标签: python dictionary


【解决方案1】:

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

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(也称为多重集)是数据最自然的数据结构(一种元素可以多次属于的集合类型,或者等效地 - 具有语义 Element -> OccurrenceCount 的映射。您可以一开始就使用它,而不是元组列表。


也可以:

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

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

类似于 dict.update() 但添加计数而不是替换它们。 Source 可以是可迭代的、字典或另一个 Counter 实例。

这就是我们想要的。让我们用与reduce 兼容的函数来包装它,看看会发生什么。

def updateInPlace(a,b):
    a.update(b)
    return a

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

这仅比 OP 的解决方案慢一点。

基准测试http://ideone.com/7IzSx (通过 astynax 更新了另一个解决方案)

(另外:如果你非常想要一个单行,你可以用 lambda x,y: x.update(y) or x 替换 updateInPlace,它的工作方式相同,甚至被证明更快,但在可读性方面失败。不要' t :-))

【讨论】:

  • 时间复杂度如何?是不是比OP的代码效率更高
  • 我不这么认为。 OP 的代码不会创建任何直接对象,因此它通常应该更有效。
  • 这种方法确实是这里介绍的最慢的方法,而 OP 的原始解决方案是最快的。这里没有惊喜。 ideone.com/HAmvi
  • 用更有效的方法修复。基准测试:ideone.com/2eGQi
  • @Kos,我稍微改变了benchmark。最快的方式:defaultdict + chain(中间是我改写的merge_with):)
【解决方案2】:
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())

【讨论】:

  • 您甚至可以使用sum 删除for 循环:count = sum((Counter(y) for y in inp), Counter())
  • @Duncan 谢谢我从来不知道,建议已实施。
  • 如果他们已经是计数器,则只需 count = sum(inp, Counter())。 :D
【解决方案3】:

你可以使用 itertools 的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 将使您的代码更清晰。

【讨论】:

  • 代替 lambda,您也可以放入 operator.itemgetter(0) :)
  • 错误:groupby,正如您提到的文档中所说,需要先排序!在这里这是可行的,因为b[1]c[0]chain(a,b,c) 中将是连续的,但是如果您改为使用chain(a,c,b),则结果不正确(对于output['15.5'],您会得到100 而不是200)...
  • 我猜它的个人喜好,但我发现这比 defaultdict 更难阅读,而且它比 OP 方法慢
  • @Emmanuel Thanx 指出了这一点。修好了。
  • 不客气,很高兴想到groupby
【解决方案4】:

你可以使用 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.iteritems(): # ".. 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].iteritems()) # ".. in dicts[0]" for lists of tuples
        for dic in dicts[1:]:
            for key, val in dic.iteritems(): # ".. 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__ 方法:

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-03-08
    • 2020-03-29
    • 1970-01-01
    • 1970-01-01
    • 2022-01-19
    • 1970-01-01
    • 2012-08-03
    • 1970-01-01
    相关资源
    最近更新 更多