Esiste un modo pitonico per combinare due dadi (aggiungere valori per le chiavi che appaiono in entrambi)?


477

Ad esempio ho due dicts:

Dict A: {'a': 1, 'b': 2, 'c': 3}
Dict B: {'b': 3, 'c': 4, 'd': 5}

Ho bisogno di un modo pitonico di "combinare" due dadi in modo tale che il risultato sia:

{'a': 1, 'b': 5, 'c': 7, 'd': 5}

Vale a dire: se una chiave appare in entrambi i dadi, aggiungi i loro valori, se appare in un solo comando, mantieni il suo valore.

Risposte:


835

Utilizzare collections.Counter:

>>> from collections import Counter
>>> A = Counter({'a':1, 'b':2, 'c':3})
>>> B = Counter({'b':3, 'c':4, 'd':5})
>>> A + B
Counter({'c': 7, 'b': 5, 'd': 5, 'a': 1})

I contatori sono fondamentalmente una sottoclasse di dict, quindi puoi ancora fare tutto il resto con loro che faresti normalmente con quel tipo, come iterare sulle loro chiavi e valori.


4
Quali sono i contatori multipli da unire in questo modo? sum(counters)purtroppo non funziona.
Dr. Jan-Philip Gehrcke,

27
@ Jan-PhilipGehrcke: dai sum()un valore iniziale, con sum(counters, Counter()).
Martijn Pieters

5
Grazie. Tuttavia, questo metodo è influenzato dalla creazione di oggetti intermedi come lo sono le stringhe di somma, giusto?
Dr. Jan-Philip Gehrcke,

6
@ Jan-PhilipGehrcke: l'altra opzione è quella di utilizzare un ciclo e +=fare un riepilogo sul posto. res = counters[0], quindi for c in counters[1:]: res += c.
Martijn Pieters

3
Mi piace questo approccio! Se qualcuno calibro mantenere le cose vicino ai dizionari di elaborazione, si potrebbe anche usare update()al posto di +=: for c in counters[1:]: res.update(c).
Dr. Jan-Philip Gehrcke,

119

Una soluzione più generica, che funziona anche per valori non numerici:

a = {'a': 'foo', 'b':'bar', 'c': 'baz'}
b = {'a': 'spam', 'c':'ham', 'x': 'blah'}

r = dict(a.items() + b.items() +
    [(k, a[k] + b[k]) for k in set(b) & set(a)])

o ancora più generico:

def combine_dicts(a, b, op=operator.add):
    return dict(a.items() + b.items() +
        [(k, op(a[k], b[k])) for k in set(b) & set(a)])

Per esempio:

>>> a = {'a': 2, 'b':3, 'c':4}
>>> b = {'a': 5, 'c':6, 'x':7}

>>> import operator
>>> print combine_dicts(a, b, operator.mul)
{'a': 10, 'x': 7, 'c': 24, 'b': 3}

27
È inoltre possibile utilizzare for k in b.viewkeys() & a.viewkeys(), quando si utilizza Python 2.7 , e saltare la creazione di set.
Martijn Pieters

Perché set(a)restituisce il set di chiavi piuttosto che il set di tuple? Qual è la logica di questo?
Sarsaparilla,

1
@HaiPhan: perché i dadi ripetono le chiavi, non le coppie kv. cf list({..}), for k in {...}ecc.
Georg

2
@Craicerjack: sì, ero solito operator.mulchiarire che questo codice è generico e non si limita all'aggiunta di numeri.
Georg

6
Potresti aggiungere un'opzione compatibile con Python 3? {**a, **b, **{k: op(a[k], b[k]) for k in a.keys() & b}}dovrebbe funzionare in Python 3.5+.
vaultah,

66
>>> A = {'a':1, 'b':2, 'c':3}
>>> B = {'b':3, 'c':4, 'd':5}
>>> c = {x: A.get(x, 0) + B.get(x, 0) for x in set(A).union(B)}
>>> print(c)

{'a': 1, 'c': 7, 'b': 5, 'd': 5}

1
L'uso non for x in set(itertools.chain(A, B))sarebbe più logico? Come usare set su dict è un po 'una sciocchezza in quanto i tasti sono già unici? So che è solo un altro modo per ottenere un set di chiavi, ma lo trovo più confuso rispetto all'uso itertools.chain(implicando che tu sappia cosa itertools.chainfa)
jeromej,

45

Introduzione: ci sono le (probabilmente) migliori soluzioni. Ma devi conoscerlo e ricordarlo e a volte devi sperare che la tua versione di Python non sia troppo vecchia o qualunque sia il problema.

Poi ci sono le soluzioni più "confuse". Sono grandi e brevi ma a volte sono difficili da capire, da leggere e da ricordare.

Vi è, tuttavia, un'alternativa che consiste nel cercare di reinventare la ruota. - Perché reinventare la ruota? - Generalmente perché è un ottimo modo per imparare (e talvolta solo perché lo strumento già esistente non fa esattamente quello che vorresti e / o il modo in cui lo desideri) e il modo più semplice se non lo sai o non ricordare lo strumento perfetto per il tuo problema.

Quindi , propongo di reinventare la ruota della Counterclasse dal collectionsmodulo (almeno in parte):

class MyDict(dict):
    def __add__(self, oth):
        r = self.copy()

        try:
            for key, val in oth.items():
                if key in r:
                    r[key] += val  # You can custom it here
                else:
                    r[key] = val
        except AttributeError:  # In case oth isn't a dict
            return NotImplemented  # The convention when a case isn't handled

        return r

a = MyDict({'a':1, 'b':2, 'c':3})
b = MyDict({'b':3, 'c':4, 'd':5})

print(a+b)  # Output {'a':1, 'b': 5, 'c': 7, 'd': 5}

Probabilmente ci sarebbero altri modi per implementarlo e ci sono già degli strumenti per farlo, ma è sempre bello visualizzare come le cose sostanzialmente funzionerebbero.


3
Bello anche per quelli di noi ancora al 2.6
Brian B,

13
myDict = {}
for k in itertools.chain(A.keys(), B.keys()):
    myDict[k] = A.get(k, 0)+B.get(k, 0)

13

Quello senza importazioni extra!

Il loro è uno standard pitonico chiamato EAFP (più facile chiedere perdono che autorizzazione). Il codice seguente si basa su quello standard di Python .

# The A and B dictionaries
A = {'a': 1, 'b': 2, 'c': 3}
B = {'b': 3, 'c': 4, 'd': 5}

# The final dictionary. Will contain the final outputs.
newdict = {}

# Make sure every key of A and B get into the final dictionary 'newdict'.
newdict.update(A)
newdict.update(B)

# Iterate through each key of A.
for i in A.keys():

    # If same key exist on B, its values from A and B will add together and
    # get included in the final dictionary 'newdict'.
    try:
        addition = A[i] + B[i]
        newdict[i] = addition

    # If current key does not exist in dictionary B, it will give a KeyError,
    # catch it and continue looping.
    except KeyError:
        continue

EDIT: grazie a Jerzyk per i suoi suggerimenti di miglioramento.


5
L'algoritmo n ^ 2 sarà significativamente più lento del metodo Counter
Joop,

@DeveshSaini meglio, ma ancora non ottimale :) es: hai davvero bisogno di smistamento? e poi, perché due anelli? hai già tutte le chiavi nel newdict, solo piccoli suggerimenti per ottimizzare
Jerzyk

Algoritmo n ^ 1 invece del precedente algoritmo n ^ 2 @Joop
Devesh Saini

11

Sicuramente la somma di Counter()s è il modo più pitone di procedere in questi casi, ma solo se risulta in un valore positivo . Ecco un esempio e come puoi vedere non c'è nessun crisultato dopo aver negato il cvalore del Bdizionario.

In [1]: from collections import Counter

In [2]: A = Counter({'a':1, 'b':2, 'c':3})

In [3]: B = Counter({'b':3, 'c':-4, 'd':5})

In [4]: A + B
Out[4]: Counter({'d': 5, 'b': 5, 'a': 1})

Questo perché gli Counters sono stati progettati principalmente per funzionare con numeri interi positivi per rappresentare conteggi correnti (il conteggio negativo non ha senso). Ma per aiutare con questi casi d'uso, Python documenta le restrizioni minime di intervallo e tipo come segue:

  • La stessa classe Counter è una sottoclasse del dizionario senza restrizioni su chiavi e valori. I valori sono intesi come numeri che rappresentano i conteggi, ma è possibile memorizzare qualsiasi cosa nel campo valore.
  • Il most_common()metodo richiede solo che i valori siano ordinabili.
  • Per operazioni sul posto come c[key] += 1, ad esempio , il tipo di valore deve supportare solo addizioni e sottrazioni. Quindi frazioni, float e decimali funzionerebbero e i valori negativi sono supportati. Lo stesso vale anche per update()e subtract()che consentono valori negativi e zero sia per gli ingressi che per le uscite.
  • I metodi multiset sono progettati solo per casi d'uso con valori positivi. Gli input possono essere negativi o zero, ma vengono creati solo output con valori positivi. Non ci sono restrizioni di tipo, ma il tipo di valore deve supportare l'addizione, la sottrazione e il confronto.
  • Il elements()metodo richiede conteggi interi. Ignora i conteggi zero e negativi.

Quindi per Counter.updateovviare a questo problema dopo aver sommato il tuo Counter puoi usare per ottenere l'output desiderato. Funziona come dict.update()ma aggiunge conteggi invece di sostituirli.

In [24]: A.update(B)

In [25]: A
Out[25]: Counter({'d': 5, 'b': 5, 'a': 1, 'c': -1})

10
import itertools
import collections

dictA = {'a':1, 'b':2, 'c':3}
dictB = {'b':3, 'c':4, 'd':5}

new_dict = collections.defaultdict(int)
# use dict.items() instead of dict.iteritems() for Python3
for k, v in itertools.chain(dictA.iteritems(), dictB.iteritems()):
    new_dict[k] += v

print dict(new_dict)

# OUTPUT
{'a': 1, 'c': 7, 'b': 5, 'd': 5}

O

In alternativa puoi usare Counter come @Martijn ha menzionato sopra.


7

Per un modo più generico ed estensibile controlla mergedict . Utilizza singledispatche può unire i valori in base ai suoi tipi.

Esempio:

from mergedict import MergeDict

class SumDict(MergeDict):
    @MergeDict.dispatch(int)
    def merge_int(this, other):
        return this + other

d2 = SumDict({'a': 1, 'b': 'one'})
d2.merge({'a':2, 'b': 'two'})

assert d2 == {'a': 3, 'b': 'two'}

5

Da Python 3.5: fusione e somma

Grazie a @tokeinizer_fsj che mi ha detto in un commento che non ho capito completamente il significato della domanda (ho pensato che aggiungere significasse semplicemente aggiungere chiavi che alla fine erano diverse nei due dittinari e, invece, intendevo che i valori chiave comuni dovrebbe essere sommato). Quindi ho aggiunto quel ciclo prima della fusione, in modo che il secondo dizionario contenga la somma delle chiavi comuni. L'ultimo dizionario sarà quello i cui valori dureranno nel nuovo dizionario che è il risultato della fusione dei due, quindi penso che il problema sia risolto. La soluzione è valida da Python 3.5 e versioni successive.

a = {
    "a": 1,
    "b": 2,
    "c": 3
}

b = {
    "a": 2,
    "b": 3,
    "d": 5
}

# Python 3.5

for key in b:
    if key in a:
        b[key] = b[key] + a[key]

c = {**a, **b}
print(c)

>>> c
{'a': 3, 'b': 5, 'c': 3, 'd': 5}

Codice riutilizzabile

a = {'a': 1, 'b': 2, 'c': 3}
b = {'b': 3, 'c': 4, 'd': 5}


def mergsum(a, b):
    for k in b:
        if k in a:
            b[k] = b[k] + a[k]
    c = {**a, **b}
    return c


print(mergsum(a, b))

Questo modo di unire i dizionari non sta aggiungendo i valori per le chiavi comuni. Nella domanda, il valore desiderato per key bè 5(2 + 3), ma il metodo sta tornando 3.
tokenizer_fsj

4

Inoltre, si noti che a.update( b )è 2 volte più veloce dia + b

from collections import Counter
a = Counter({'menu': 20, 'good': 15, 'happy': 10, 'bar': 5})
b = Counter({'menu': 1, 'good': 1, 'bar': 3})

%timeit a + b;
## 100000 loops, best of 3: 8.62 µs per loop
## The slowest run took 4.04 times longer than the fastest. This could mean that an intermediate result is being cached.

%timeit a.update(b)
## 100000 loops, best of 3: 4.51 µs per loop

2
def merge_with(f, xs, ys):
    xs = a_copy_of(xs) # dict(xs), maybe generalizable?
    for (y, v) in ys.iteritems():
        xs[y] = v if y not in xs else f(xs[x], v)

merge_with((lambda x, y: x + y), A, B)

Si potrebbe facilmente generalizzare questo:

def merge_dicts(f, *dicts):
    result = {}
    for d in dicts:
        for (k, v) in d.iteritems():
            result[k] = v if k not in result else f(result[k], v)

Quindi può richiedere un numero qualsiasi di dadi.


2

Questa è una soluzione semplice per unire due dizionari dove +=può essere applicato ai valori, deve iterare su un dizionario solo una volta

a = {'a':1, 'b':2, 'c':3}

dicts = [{'b':3, 'c':4, 'd':5},
         {'c':9, 'a':9, 'd':9}]

def merge_dicts(merged,mergedfrom):
    for k,v in mergedfrom.items():
        if k in merged:
            merged[k] += v
        else:
            merged[k] = v
    return merged

for dct in dicts:
    a = merge_dicts(a,dct)
print (a)
#{'c': 16, 'b': 5, 'd': 14, 'a': 10}

1

Questa soluzione è facile da usare, viene utilizzata come un normale dizionario, ma è possibile utilizzare la funzione di somma.

class SumDict(dict):
    def __add__(self, y):
        return {x: self.get(x, 0) + y.get(x, 0) for x in set(self).union(y)}

A = SumDict({'a': 1, 'c': 2})
B = SumDict({'b': 3, 'c': 4})  # Also works: B = {'b': 3, 'c': 4}
print(A + B)  # OUTPUT {'a': 1, 'b': 3, 'c': 6}

1

Che dire:

def dict_merge_and_sum( d1, d2 ):
    ret = d1
    ret.update({ k:v + d2[k] for k,v in d1.items() if k in d2 })
    ret.update({ k:v for k,v in d2.items() if k not in d1 })
    return ret

A = {'a': 1, 'b': 2, 'c': 3}
B = {'b': 3, 'c': 4, 'd': 5}

print( dict_merge_and_sum( A, B ) )

Produzione:

{'d': 5, 'a': 1, 'c': 7, 'b': 5}

0

Le soluzioni di cui sopra sono ottime per lo scenario in cui hai un piccolo numero di Counters. Se ne hai una grande lista, qualcosa del genere è molto più bello:

from collections import Counter

A = Counter({'a':1, 'b':2, 'c':3})
B = Counter({'b':3, 'c':4, 'd':5}) 
C = Counter({'a': 5, 'e':3})
list_of_counts = [A, B, C]

total = sum(list_of_counts, Counter())

print(total)
# Counter({'c': 7, 'a': 6, 'b': 5, 'd': 5, 'e': 3})

La soluzione di cui sopra sta essenzialmente riassumendo la Counters per:

total = Counter()
for count in list_of_counts:
    total += count
print(total)
# Counter({'c': 7, 'a': 6, 'b': 5, 'd': 5, 'e': 3})

Questo fa la stessa cosa, ma penso che aiuti sempre a vedere cosa sta effettivamente facendo sotto.


0

Unire tre dadi a, b, c in una sola riga senza altri moduli o librerie

Se abbiamo i tre dadi

a = {"a":9}
b = {"b":7}
c = {'b': 2, 'd': 90}

Unisci tutto con una sola riga e restituisci un oggetto dict usando

c = dict(a.items() + b.items() + c.items())

ritornando

{'a': 9, 'b': 2, 'd': 90}

6
Rileggi la domanda, questo non è l'output previsto. Avrebbe dovuto essere con i tuoi ingressi: {'a': 9, 'b': 9, 'd': 90}. Manca il requisito "somma".
Patrick Mevzek,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.