Python: Элегантно объединить словари с суммой() значений

Я пытаюсь объединить журналы с нескольких серверов. Каждый журнал представляет собой список кортежей (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}

Как и ожидалось.

Я собираюсь пойти бананами из-за коллеги, который видел код. Она настаивает на том, что для этого должен быть более питоновский и элегантный способ, без этих вложенных циклов. Любые идеи?

Ответ 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) unbound collections.Counter method

Подобно dict.update(), но добавляйте количество вместо вместо них. Источник может быть итерируемым, словарем или другим экземпляром экземпляра.

Это то, что мы хотим. Позвольте обернуть его функцией, совместимой с reduce, и посмотреть, что произойдет.

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

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

Это лишь незначительно медленнее, чем решение OP.

Benchmark: http://ideone.com/7IzSx (Обновлено с помощью еще одного решения, благодаря astynax)

(Также: Если вы отчаянно хотите однострочный, вы можете заменить updateInPlace на lambda x,y: x.update(y) or x, который работает одинаково и даже окажется на несколько секунд быстрее, но с ошибкой не читается. Не: ))

Ответ 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})

Edit: В качестве duncan вы можете заменить эти 3 строки одной строкой:

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

заменить на: count = sum((Counter(y) for y in inp), Counter())

Ответ 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 сделает код более понятным.

Ответ 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__: