Python: fusionner élégamment les dictionnaires avec sum() de valeurs [dupliquer]

Cette question a déjà une réponse ici:

J'essaie de fusionner les journaux de plusieurs serveurs. Chaque journal est une liste de tuples (date, count). date peut apparaître plus d'une fois, et je veux que le dictionnaire résultant contienne la somme de tous compte de tous les serveurs.

Voici ma tentative, avec quelques données par exemple:

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)

Qui donne:

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

Comme prévu.

Je suis sur le point de devenir folle à cause d'un collègue qui a vu le code. Elle insiste sur le fait qu'il doit y avoir une façon plus pythonique et élégante de le faire, sans ces boucles imbriquées. Des idées?

26
demandé sur sloth 2012-07-02 12:24:38

4 réponses

Ne devient pas plus simple que cela, je pense:

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())

Notez que Counter (Également connu sous le nom de multiset) est la structure de données la plus naturelle pour vos données (un type d'ensemble auquel les éléments peuvent appartenir plus d'une fois, ou de manière équivalente - une carte avec élément sémantique -> OccurrenceCount. Vous auriez pu l'utiliser en premier lieu, au lieu de listes de tuples.


Aussi possible:

from collections import Counter
from operator import add

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

Utiliser reduce(add, seq) au lieu de sum(seq, initialValue) est généralement plus souple et vous permet de passer passage de la valeur initiale redondante.

Notez que vous pouvez également utiliser operator.and_ pour trouver l'intersection des multisets au lieu de la somme.


La variante ci-dessus est terriblement lente, car un nouveau compteur est créé à chaque étape. Nous allons remédier à cela.

Nous savons que Counter+Counter renvoie un nouveau Counter avec des données fusionnées. C'est OK, mais nous voulons éviter la création supplémentaire. Utilisons Counter.update à la place:

Mise à Jour(auto, itérable=None, **kwds) indépendant des collections.Compteur méthode

Comme dict.update () mais ajouter des comptes au lieu de les remplacer. Source peut être un itérable, un dictionnaire ou une autre instance de compteur.

C'est ce que nous voulons. Enveloppons-le avec une fonction compatible avec reduce et voyons ce qui se passe.

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

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

Ce n'est que légèrement plus lent que la solution de L'OP.

Référence: http://ideone.com/7IzSx (mis à Jour avec encore une autre solution, grâce à Astynax)

(aussi: si vous voulez désespérément un one-liner, vous pouvez remplacer updateInPlace par lambda x,y: x.update(y) or x qui fonctionne de la même manière et s'avère même une fraction de seconde plus rapide, mais échoue à la lisibilité. Non :-))

30
répondu Kos 2012-07-02 12:26:20
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)

Sortie:

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

Modifier: Comme duncan suggéré, vous pouvez remplacer ces 3 lignes avec une seule ligne:

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

Remplacer par : count = sum((Counter(y) for y in inp), Counter())

8
répondu Ashwini Chaudhary 2017-05-23 12:18:07

Vous pouvez utiliser 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

L'utilisation de groupby au lieu de deux boucles et d'un {[2] } rendra votre code plus clair.

7
répondu sloth 2012-07-02 08:52:00

Vous pouvez utiliser le Compteur ou defaultdict, ou vous pouvez essayer ma variante:

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}

, Ou encore plus générique:

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']}

Vous pouvez également sous-classer le dict et implémenter une méthode __add__:

1
répondu astynax 2012-07-02 10:00:34