Existe alguma maneira pitônica de combinar dois ditados (adicionando valores para chaves que aparecem em ambos)?

477

Por exemplo, eu tenho dois ditados:

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

Eu preciso de uma maneira pitônica de 'combinar' dois ditados, para que o resultado seja:

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

Ou seja: se uma chave aparecer em ambos os ditados, adicione seus valores; se aparecer em apenas um ditado, mantenha seu valor.

Derrick Zhang
fonte

Respostas:

835

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

Os contadores são basicamente uma subclasse de dict, então você ainda pode fazer tudo o que normalmente faria com esse tipo, como iterar sobre suas chaves e valores.

Martijn Pieters
fonte
4
O que há de vários contadores para mesclar assim? sum(counters)infelizmente não funciona.
Dr. Jan-Philip Gehrcke
27
@ Jan-PhilipGehrcke: Dê sum()um valor inicial, com sum(counters, Counter()).
Martijn Pieters
5
Obrigado. No entanto, esse método é afetado pela criação de objetos intermediários, pois as seqüências de soma são, certo?
Dr. Jan-Philip Gehrcke
6
@ Jan-PhilipGehrcke: Sua outra opção é usar um loop e +=fazer a soma no local. res = counters[0]então for c in counters[1:]: res += c.
Martijn Pieters
3
Eu gosto dessa abordagem! Se alguém gosta de manter as coisas perto de dicionários de processamento, pode-se também usar update()em vez de +=: for c in counters[1:]: res.update(c).
Dr. Jan-Philip Gehrcke
119

Uma solução mais genérica, que também funciona para valores não 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)])

ou ainda mais 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 exemplo:

>>> 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
fonte
27
Você também pode usar for k in b.viewkeys() & a.viewkeys(), ao usar python 2.7 , e pular a criação de conjuntos.
Martijn Pieters
Por que set(a)retornar o conjunto de chaves em vez do conjunto de tuplas? Qual é a justificativa para isso?
Salsaparrilha
1
@HaiPhan: porque os ditados repetem as chaves, não os pares kv. cf list({..}), for k in {...}etc
georg 01/04
2
@ Craicerjack: sim, eu deixava operator.mulclaro que esse código é genérico e não se limita a adicionar números.
Georg
6
Você poderia adicionar uma opção compatível com Python 3? {**a, **b, **{k: op(a[k], b[k]) for k in a.keys() & b}}deve funcionar em 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
fonte
1
Usar não for x in set(itertools.chain(A, B))seria mais lógico? Como usar set on dict é um absurdo, já que as chaves já são únicas? Eu sei que é apenas outra maneira de obter um conjunto de chaves, mas acho que é mais confuso do que usar itertools.chain(o que implica que você sabe o que itertools.chainfaz)
jeromej
45

Introdução: Existem as (provavelmente) melhores soluções. Mas você precisa conhecê-lo e lembrá-lo e, às vezes, espera que sua versão do Python não seja muito antiga ou seja qual for o problema.

Depois, há as soluções mais 'hacky'. Eles são ótimos e curtos, mas às vezes são difíceis de entender, ler e lembrar.

Existe, no entanto, uma alternativa que é tentar reinventar a roda. - Por que reinventar a roda? - Geralmente porque é uma maneira muito boa de aprender (e às vezes apenas porque a ferramenta já existente não faz exatamente o que você gostaria e / ou da maneira que você gostaria) e a maneira mais fácil se você não conhece ou não lembre-se da ferramenta perfeita para o seu problema.

Então , proponho reinventar a roda da Counterclasse a partir do collectionsmódulo (pelo 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}

Provavelmente haveria outras maneiras de implementar isso e já existem ferramentas para fazer isso, mas é sempre bom visualizar como as coisas funcionariam basicamente.

jeromej
fonte
3
Bom para aqueles de nós que ainda estão no 2.6 também #
Brian B
13
myDict = {}
for k in itertools.chain(A.keys(), B.keys()):
    myDict[k] = A.get(k, 0)+B.get(k, 0)

fonte
13

Aquele sem importações extras!

É um padrão pitônico chamado EAFP (mais fácil pedir perdão do que permissão). O código abaixo é baseado nesse padrão 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: obrigado a Jerzyk por suas sugestões de melhoria.

Devesh Saini
fonte
5
O algoritmo n ^ 2 será significativamente mais lento que o método Counter
Joop
@DeveshSaini melhor, mas ainda abaixo do ideal :) por exemplo: você realmente precisa de classificação? e então, por que dois loops? você já tem todas as chaves na newdict, apenas pequenas dicas para otimizar
Jerzyk
n ^ 1 algoritmo foi colocado em vez de n ^ 2 algoritmo anterior @Joop
Devesh Saini
11

Definitivamente, somar os Counter()s é a maneira mais pitônica de seguir nesses casos, mas apenas se resultar em um valor positivo . Aqui está um exemplo e, como você pode ver, não há cresultado após negar o cvalor do Bdicionário.

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

Isso Counterocorre porque s foram projetados principalmente para trabalhar com números inteiros positivos para representar contagens em execução (contagem negativa não faz sentido). Mas, para ajudar nesses casos de uso, o python documenta as restrições mínimas de alcance e tipo da seguinte maneira:

  • A própria classe Counter é uma subclasse de dicionário sem restrições em suas chaves e valores. Os valores devem ser números que representam contagens, mas você pode armazenar qualquer coisa no campo de valor.
  • O most_common()método requer apenas que os valores sejam ordenáveis.
  • Para operações no local, como c[key] += 1, o tipo de valor precisa apenas suportar adição e subtração. Portanto, frações, flutuadores e decimais funcionariam e valores negativos são suportados. O mesmo vale para update()e subtract()que permitem valores negativos e zero para entradas e saídas.
  • Os métodos multiset são projetados apenas para casos de uso com valores positivos. As entradas podem ser negativas ou zero, mas apenas saídas com valores positivos são criadas. Não há restrições de tipo, mas o tipo de valor precisa suportar adição, subtração e comparação.
  • O elements()método requer contagens de números inteiros. Ignora contagens zero e negativas.

Portanto, para contornar esse problema depois de somar seu contador, você pode usar Counter.update -lo para obter a saída desejada. Funciona como, dict.update()mas adiciona contagens, em vez de substituí-las.

In [24]: A.update(B)

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

OU

Alternativa, você pode usar o Counter como o @Martijn mencionou acima.

Adeel
fonte
7

Para uma maneira mais genérica e extensível, verifique mesclagem . Usasingledispatch e pode mesclar valores com base em seus tipos.

Exemplo:

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
fonte
5

Do python 3.5: mesclando e somando

Graças a @tokeinizer_fsj que me disse em um comentário que eu não entendi completamente o significado da pergunta (eu pensei que adicionar significava apenas adicionar chaves que eventualmente eram diferentes nos dois dicionários) e, em vez disso, eu quis dizer que os valores de chave comum deve ser resumido). Então, eu adicionei esse loop antes da mesclagem, para que o segundo dicionário contenha a soma das chaves comuns. O último dicionário será aquele cujos valores durarão no novo dicionário que é o resultado da fusão dos dois, então eu acho que o problema está resolvido. A solução é válida no python 3.5 e nas seguintes versões.

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 reutilizável

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
fonte
Essa maneira de mesclar dicionários não está adicionando os valores para chaves comuns. Na pergunta, o valor desejado para a chave bé 5(2 + 3), mas seu método está retornando 3.
Tokenizer_fsj #
4

Além disso, observe que a.update( b )é 2x mais 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
devia ver
fonte
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)

Você pode facilmente generalizar isso:

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)

Então pode levar qualquer número de ditados.

Jonas Kölker
fonte
2

Esta é uma solução simples para mesclar dois dicionários, onde +=pode ser aplicada aos valores; ela precisa repetir o dicionário apenas uma 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
fonte
1

Esta solução é fácil de usar, é usada como um dicionário normal, mas você pode usar a função soma.

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
fonte
1

A respeito:

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

Resultado:

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

As soluções acima são ótimas para o cenário em que você possui um pequeno número de Counters. Se você tem uma grande lista deles, algo assim é muito melhor:

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

A solução acima está basicamente somando os 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})

Isso faz a mesma coisa, mas acho que sempre ajuda a ver o que está efetivamente fazendo por baixo.

Michael Hall
fonte
0

Mesclando três ditados a, b, c em uma única linha sem outros módulos ou bibliotecas

Se temos os três ditados

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

Mesclar tudo com uma única linha e retornar um objeto dict usando

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

Retornando

{'a': 9, 'b': 2, 'd': 90}
user6830669
fonte
6
Releia a pergunta, este não é o resultado esperado. Deve ter sido com suas entradas: {'a': 9, 'b': 9, 'd': 90}. Está faltando o requisito "soma".
Patrick Mevzek