2012-07-02 17 views
26

Sto cercando di unire log da diversi server. Ogni registro è un elenco di tuple (date, count). date potrebbe apparire più di una volta e voglio che il dizionario risultante contenga la somma di tutti i conteggi di tutti i server.Python: Unire elegantemente dizionari con sum() di valori

Ecco il mio tentativo, con alcuni dati, ad esempio:

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) 

Che dà:

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

come previsto.

Sto per andare alle banane a causa di un collega che ha visto il codice. Insiste sul fatto che ci deve essere un modo più pitonico ed elegante per farlo, senza questi cicli annidati. Qualche idea?

+4

uso 'Counter()' –

+2

@AshwiniChaudhary: 'contatore()' solo conta le occorrenze e, poiché i valori sono già pre-compilati, non funzionerà per questo scenario. –

+0

@ChristianWitts vedere la mia soluzione di seguito. –

risposta

30

non ottiene più semplice di questo, penso:

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

noti che Counter (noto anche come un multiset) è la struttura dei dati più naturale per i vostri dati (un tipo di set a cui elementi possono appartenere più di una volta, o equivalentemente - una mappa con semantica Elemento -> Ricorrenza. Potresti averlo usato in primo luogo, invece di liste di tuple.


anche possibile:

from collections import Counter 
from operator import add 

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

utilizzando reduce(add, seq) anziché sum(seq, initialValue) è generalmente più flessibile e consente di saltare passando il valore iniziale ridondante.

Si noti che è anche possibile utilizzare operator.and_ per trovare l'intersezione dei multiset anziché la somma.


La variante precedente è terribilmente lenta, perché un nuovo contatore viene creato ad ogni passaggio. Risolviamolo.

Sappiamo che Counter+Counter restituisce un nuovo Counter con dati uniti.Questo è OK, ma vogliamo evitare la creazione extra. Usiamo Counter.update invece:

aggiornamento (self, iterable = None, ** kwds) Metodo collections.Counter non legato

Come dict.update(), ma aggiungere conta invece di sostituirli. L'origine può essere un iterabile, un dizionario o un'altra istanza Counter.

Questo è quello che vogliamo. Prendiamolo con una funzione compatibile con reduce e vediamo cosa succede.

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

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

Questo è solo leggermente più lento della soluzione dell'OP.

Benchmark: http://ideone.com/7IzSx(Aggiornato con un'altra soluzione, grazie alla astynax)

(anche: Se si desidera disperatamente un one-liner, è possibile sostituire updateInPlace di lambda x,y: x.update(y) or x che funziona il allo stesso modo e si dimostra anche essere una frazione di secondo più veloce, ma non riesce a leggibilità.Non :-))

+2

+1 Mi piace molto questa soluzione. – sloth

+1

E la complessità del tempo? È più efficiente del codice OP – jerrymouse

+0

Non penso. Il codice dell'OP non crea oggetti immediati, quindi dovrebbe essere generalmente più efficiente. – Kos

7

Si potrebbe utilizzare 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'uso di groupby invece di due anelli e un defaultdict renderà il vostro codice più chiaro.

+2

invece di lambda, puoi anche inserire un 'operator.itemgetter (0)' :) – Kos

+1

Sbagliato: 'groupby', come detto in il documento che hai menzionato, ha bisogno di essere ordinato prima! Qui funziona perché 'b [1]' e 'c [0]' saranno consecutivi in ​​'chain (a, b, c)' ma se fai 'chain (a, c, b)', invece, il risultato è non corretto (ottieni 100 invece di 200 per 'output ['15 .5 ']') ... – Emmanuel

+1

Immagino il suo gusto personale, ma trovo questo più difficile da leggere rispetto a defaultdict, e anche più lento dell'approccio OP – fraxel

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

uscita:

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

Edit: Come duncan suggerito è possibile sostituire questi 3 linee con una sola riga:

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

sostituire con: count = sum((Counter(y) for y in inp), Counter())

012.351.641.061.
+2

Si può anche rimuovere il ciclo 'for' usando' sum': 'count = sum ((Counter (y) per y in inp), Counter())' – Duncan

+0

@Duncan grazie non l'ho mai saputo, suggerito . –

1

È possibile utilizzare Counter o defaultdict, oppure è possibile provare la mia 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} 

O ancora più generico:

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

Inoltre è possibile creare una sottoclasse del dict e implementare un metodo __add__:

+1

Grazie! Sembra un po 'meno chiaro del codice originale, però. –

Problemi correlati