¿Hay alguna forma pitónica de combinar dos dictados (agregando valores para las claves que aparecen en ambos)?

477

Por ejemplo, tengo dos dictados:

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

Necesito una forma pitónica de 'combinar' dos dictados de modo que el resultado sea:

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

Es decir: si una clave aparece en ambos dictados, agregue sus valores, si aparece en un solo dict, mantenga su valor.

Derrick Zhang
fuente

Respuestas:

835

Uso 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})

Los contadores son básicamente una subclase de dict, por lo que aún puede hacer todo lo demás con ellos que normalmente haría con ese tipo, como iterar sobre sus claves y valores.

Martijn Pieters
fuente
44
¿Qué hay de múltiples contadores para fusionar así? sum(counters)no funciona, desafortunadamente
Dr. Jan-Philip Gehrcke
27
@ Jan-PhilipGehrcke: Dé sum()un valor inicial, con sum(counters, Counter()).
Martijn Pieters
55
Gracias. Sin embargo, este método se ve afectado por la creación de objetos intermedios como lo es la suma de cadenas, ¿verdad?
Dr. Jan-Philip Gehrcke
66
@ Jan-PhilipGehrcke: su otra opción es usar un bucle y +=hacer una suma in situ. res = counters[0]entonces for c in counters[1:]: res += c.
Martijn Pieters
3
Me gusta ese enfoque! Si le gusta alguien mantener las cosas cerca de los diccionarios de procesamiento, también se podría utilizar update()en lugar de +=: for c in counters[1:]: res.update(c).
Dr. Jan-Philip Gehrcke
119

Una solución más genérica, que también funciona para valores no numéricos:

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 incluso más genérico:

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

Por ejemplo:

>>> 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}
georg
fuente
27
También puede usar for k in b.viewkeys() & a.viewkeys(), cuando usa Python 2.7 , y omitir la creación de conjuntos.
Martijn Pieters
¿Por qué set(a)devuelve el conjunto de claves en lugar de un conjunto de tuplas? ¿Cuál es la razón de esto?
Zarzaparrilla 01 de
1
@HaiPhan: porque los dictos iteran sobre las claves, no sobre los pares kv. cf list({..}), for k in {...}etc
georg
2
@Craicerjack: sí, solía operator.muldejar claro que este código es genérico y no se limita a agregar números.
georg
66
¿Podría agregar una opción compatible con Python 3? {**a, **b, **{k: op(a[k], b[k]) for k in a.keys() & b}}debería funcionar en 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}
Ashwini Chaudhary
fuente
1
¿No for x in set(itertools.chain(A, B))sería más lógico usar ? Como usar set on dict es un poco absurdo ya que las teclas ya son únicas? Sé que es solo otra forma de obtener un conjunto de claves, pero me resulta más confuso que usarlo itertools.chain(lo que implica que sabes lo que itertools.chainhace)
jeromej
45

Introducción: Existen las (probablemente) mejores soluciones. Pero debe saberlo y recordarlo, y a veces tiene que esperar que su versión de Python no sea demasiado antigua o sea cual sea el problema.

Luego están las soluciones más 'hacky'. Son geniales y cortos, pero a veces son difíciles de entender, leer y recordar.

Sin embargo, existe una alternativa que consiste en intentar reinventar la rueda. - ¿Por qué reinventar la rueda? - Generalmente porque es una muy buena manera de aprender (y a veces solo porque la herramienta ya existente no hace exactamente lo que le gustaría y / o como le gustaría) y la forma más fácil si no sabe o No recuerdo la herramienta perfecta para su problema.

Por lo tanto , propongo reinventar la rueda de la Counterclase desde el collectionsmódulo (al menos parcialmente):

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}

Probablemente habría otra forma de implementar eso y ya hay herramientas para hacerlo, pero siempre es bueno visualizar cómo funcionarían las cosas básicamente.

jeromej
fuente
3
Agradable para aquellos de nosotros todavía en 2.6 también
Brian B
13
myDict = {}
for k in itertools.chain(A.keys(), B.keys()):
    myDict[k] = A.get(k, 0)+B.get(k, 0)

fuente
13

¡El que no tiene importaciones adicionales!

Es un estándar pitónico llamado EAFP (más fácil de pedir perdón que permiso). El siguiente código se basa en ese estándar de 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

EDITAR: gracias a jerzyk por sus sugerencias de mejora.

Devesh Saini
fuente
55
El algoritmo n ^ 2 será significativamente más lento que el método Counter
Joop
@DeveshSaini mejor, pero sigue siendo subóptimo :) por ejemplo: ¿realmente necesita ordenar? y luego, ¿por qué dos bucles? ya tienes todas las claves en el nuevo veredicto, solo pequeños consejos para optimizar
Jerzyk
Se ha colocado un algoritmo n ^ 1 en lugar del algoritmo n ^ 2 anterior @Joop
Devesh Saini
11

Definitivamente, sumar la Counter()s es la forma más pitónica en tales casos, pero solo si resulta en un valor positivo . Aquí hay un ejemplo y, como puede ver, no hay ningún cresultado después de negar el cvalor del Bdiccionario.

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

Esto se debe a que los Counters se diseñaron principalmente para trabajar con enteros positivos para representar recuentos en ejecución (el recuento negativo no tiene sentido). Pero para ayudar con esos casos de uso, python documenta el rango mínimo y las restricciones de tipo de la siguiente manera:

  • La clase Counter en sí es una subclase de diccionario sin restricciones en sus claves y valores. Los valores están destinados a ser números que representan recuentos, pero puede almacenar cualquier cosa en el campo de valor.
  • El most_common()método solo requiere que los valores sean ordenables.
  • Para operaciones in situ como c[key] += 1, el tipo de valor solo necesita admitir sumas y restas. Por lo tanto, las fracciones, los flotantes y los decimales funcionarían y los valores negativos son compatibles. Lo mismo también es cierto para update()y subtract()que permiten valores negativos y cero para entradas y salidas.
  • Los métodos de múltiples conjuntos están diseñados solo para casos de uso con valores positivos. Las entradas pueden ser negativas o cero, pero solo se crean salidas con valores positivos. No hay restricciones de tipo, pero el tipo de valor debe admitir suma, resta y comparación.
  • El elements()método requiere recuentos de enteros. Ignora los recuentos cero y negativo.

Entonces, para solucionar ese problema después de sumar su Contador, puede usarlo Counter.updatepara obtener el resultado deseado. Funciona como dict.update()pero agrega conteos en lugar de reemplazarlos.

In [24]: A.update(B)

In [25]: A
Out[25]: Counter({'d': 5, 'b': 5, 'a': 1, 'c': -1})
Kasramvd
fuente
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

Alternativa que puede usar Counter como @Martijn ha mencionado anteriormente.

Adeel
fuente
7

Para una forma más genérica y extensible, verifique mergedict . Utiliza singledispatchy puede fusionar valores en función de sus tipos.

Ejemplo:

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'}
schettino72
fuente
5

Desde python 3.5: fusión y suma

Gracias a @tokeinizer_fsj que me dijo en un comentario que no entendí completamente el significado de la pregunta (pensé que agregar solo significaba agregar claves que eventualmente eran diferentes en los dos dictadores y, en cambio, quería decir que los valores clave comunes debe resumirse) Así que agregué ese ciclo antes de la fusión, para que el segundo diccionario contenga la suma de las claves comunes. El último diccionario será aquel cuyos valores durarán en el nuevo diccionario que es el resultado de la fusión de los dos, así que creo que el problema está resuelto. La solución es válida desde python 3.5 y las siguientes versiones.

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}

Código reutilizable

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))
Giovanni G. PY
fuente
Esta forma de fusionar diccionarios no es agregar los valores para las claves comunes. En la pregunta, el valor deseado para la clave bes 5(2 + 3), pero su método está regresando 3.
tokenizer_fsj
4

Además, tenga en cuenta que a.update( b )es 2 veces más rápido quea + 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
debería ver
fuente
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)

Podrías generalizar esto fácilmente:

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)

Entonces puede tomar cualquier cantidad de dictados.

Jonas Kölker
fuente
2

Esta es una solución simple para fusionar dos diccionarios donde +=se pueden aplicar a los valores, tiene que iterar sobre un diccionario solo una vez

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}
ragardner
fuente
1

Esta solución es fácil de usar, se usa como un diccionario normal, pero puede usar la función de suma.

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}
Ignacio Villela
fuente
1

Qué pasa:

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

Salida:

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

Las soluciones anteriores son excelentes para el escenario donde tiene una pequeña cantidad de Counters. Sin embargo, si tiene una gran lista de ellos, algo como esto es mucho mejor:

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 solución anterior está esencialmente sumando la Counters por:

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

Esto hace lo mismo, pero creo que siempre ayuda ver lo que efectivamente está haciendo debajo.

Michael Hall
fuente
0

Fusionar tres dictados a, b, c en una sola línea sin ningún otro módulo o libs

Si tenemos los tres dictados

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

Fusionar todo con una sola línea y devolver un objeto dict usando

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

Volviendo

{'a': 9, 'b': 2, 'd': 90}
usuario6830669
fuente
66
Vuelva a leer la pregunta, este no es el resultado esperado. Debería haber estado con sus entradas: {'a': 9, 'b': 9, 'd': 90}. Te estás perdiendo el requisito de "suma".
Patrick Mevzek