Python: элегантно объединить словари с sum () значений [duplicate]

На этот вопрос уже есть ответ:

Есть ли какой-нибудь питонный способ объединить два слова (добавив значения для ключей, которые появляются в обоих)? 18 ответов

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

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

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

 Ashwini Chaudhary02 июл. 2012 г., 10:32
useCounter()
 Christian Witts02 июл. 2012 г., 10:37
@ AshwiniChaudhary:Counter() учитывает только случаи, а поскольку значения уже заполнены, для этого сценария это не сработает.
 Ashwini Chaudhary02 июл. 2012 г., 10:41
@ ChristianWitts смотрите мое решение ниже.
 DSM02 июл. 2012 г., 10:53
То, что мне кажется непитонным, тратит время на то, чтобы сделать совершенно ясный код более питоническим. Pythonicness, который нуждается в часах мысли, не является истинной Pythonicness.
 Christian Witts02 июл. 2012 г., 10:55
@ AshwiniChaudhary: Вы узнаете что-то новое каждый день

Ответы на вопрос(4)

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)

выход

Редактировать Как Duncan предложил заменить эти 3 строки одной строкой:

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

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

 Duncan02 июл. 2012 г., 11:19
Ты можешь даже удалитьfor цикл с помощьюsum: count = sum((Counter(y) for y in inp), Counter())
 Ashwini Chaudhary02 июл. 2012 г., 11:28
@ Дункан, спасибо, я никогда этого не знал, предложение реализовано.

Ты можешь использовать itertools 'группа п:

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

 fraxel02 июл. 2012 г., 10:49
Я предполагаю его личный вкус, но мне труднее его читать, чем defaultdict, а также он медленнее, чем OP-подход
 Emmanuel02 июл. 2012 г., 10:56
Не за что, приятно подумать оgroupby так или иначе
 Kos02 июл. 2012 г., 10:43
Вместо лямбды вы также можете заглянуть вoperator.itemgetter(0):)
 Emmanuel02 июл. 2012 г., 10:49
Неправильно:groupby, как сказано в документе, который вы упоминаете, сначала нужно отсортировать! Вот это работает, потому чтоb[1] а такжеc[0] будет последовательно вchain(a,b,c) но если ты сделаешьchain(a,c,b) вместо этого результат неправильный (вы получаете 100 вместо 200 дляoutput['15.5']) ...
 sloth02 июл. 2012 г., 10:52
@ Emmanuel Thanx за указание на это. Починил это
Решение Вопроса

Думаю, не проще, чем это:

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) несвязанные коллекции. Метод поиска

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

Вот чего мы хотим. Давайте обернем его функцией, совместимой сreduce и посмотри, что получится.

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

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

Это лишь немного медленнее, чем решение ОП.

Benchmark: http: //ideone.com/7IzS (Обновлено еще одним решением, благодаря Astynax)

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

 jerrymouse02 июл. 2012 г., 11:11
А как насчет временной сложности? Это более эффективно, чем код ОП
 Kos02 июл. 2012 г., 11:16
Я так не думаю. Код OP не создает непосредственных объектов, поэтому он должен быть более эффективным.
 sloth02 июл. 2012 г., 11:09
+ 1 Мне очень нравится это решение.
 astynax02 июл. 2012 г., 12:42
@ Кос, я немного изменил Тест. Самый быстрый способ:defaultdict + chain (и мой переписанныйmerge_with в центре) :
 Kos02 июл. 2012 г., 11:37
Этот подход действительно самый медленный из представленных здесь, причем оригинальное решение OP является самым быстрым. Здесь нет сюрпризов. Ideone.com / HAmvi

или вы можете попробовать мой вариант:

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__ метод:

 Adam Matan02 июл. 2012 г., 11:52
Благодарность! Хотя, кажется, он немного менее понятен, чем оригинальный код.

Ваш ответ на вопрос