Como mesclar dicionários de dicionários?


129

Preciso mesclar vários dicionários, eis o que tenho por exemplo:

dict1 = {1:{"a":{A}}, 2:{"b":{B}}}

dict2 = {2:{"c":{C}}, 3:{"d":{D}}

Com A B Ce Dsendo folhas da árvore, como{"info1":"value", "info2":"value2"}

Existe um nível desconhecido (profundidade) de dicionários, pode ser {2:{"c":{"z":{"y":{C}}}}}

No meu caso, representa uma estrutura de diretório / arquivos com nós sendo docs e deixando de ser arquivos.

Quero mesclá-los para obter:

 dict3 = {1:{"a":{A}}, 2:{"b":{B},"c":{C}}, 3:{"d":{D}}}

Não tenho certeza de como eu poderia fazer isso facilmente com o Python.


O que você deseja para a sua profundidade arbitrária de dicionários? Você quer yachatado até o cnível ou o quê? Seu exemplo está incompleto.
agf

Verifique minha classe NestedDict aqui: stackoverflow.com/a/16296144/2334951 Ele gerencia estruturas de dicionário aninhadas, como mesclagem e muito mais.
precisa saber é o seguinte

3
Um aviso para todos que procuram soluções: Esta pergunta é apenas para dicts aninhados. A maioria das respostas não lida adequadamente com o caso mais complicado de listas de dictos dentro da estrutura. Se você precisar, tente a resposta de @Osiloke abaixo: stackoverflow.com/a/25270947/1431660
SHernandez


Respostas:


143

isso é realmente bastante complicado - especialmente se você deseja uma mensagem de erro útil quando as coisas são inconsistentes, enquanto aceita corretamente entradas duplicadas mas consistentes (algo que nenhuma outra resposta aqui faz ...)

supondo que você não tenha um grande número de entradas, uma função recursiva é mais fácil:

def merge(a, b, path=None):
    "merges b into a"
    if path is None: path = []
    for key in b:
        if key in a:
            if isinstance(a[key], dict) and isinstance(b[key], dict):
                merge(a[key], b[key], path + [str(key)])
            elif a[key] == b[key]:
                pass # same leaf value
            else:
                raise Exception('Conflict at %s' % '.'.join(path + [str(key)]))
        else:
            a[key] = b[key]
    return a

# works
print(merge({1:{"a":"A"},2:{"b":"B"}}, {2:{"c":"C"},3:{"d":"D"}}))
# has conflict
merge({1:{"a":"A"},2:{"b":"B"}}, {1:{"a":"A"},2:{"b":"C"}})

note que isso muda a- o conteúdo de bé adicionado a a(que também é retornado). se você quiser ficar, apode chamar assim merge(dict(a), b).

A agf indicou (abaixo) que você pode ter mais de dois dictos; nesse caso, você pode usar:

reduce(merge, [dict1, dict2, dict3...])

onde tudo será adicionado ao dict1.

[nota - editei minha resposta inicial para alterar o primeiro argumento; isso facilita a explicação da "redução"

ps em python 3, você também precisará from functools import reduce


1
Você pode colocar isso dentro de um reduceloop equivalente ou para trabalhar com um número arbitrário de dicts em vez de dois. No entanto, também não tenho certeza de que ele faça o que ele quer (ele não estava claro). Você acaba com 2: {'c': {'z': {'y': {'info1': 'value', 'info2': 'value2'}}}, 'b': {'info1': 'value', 'info2': 'value2'}}o segundo exemplo dele, não tenho certeza se ele quer ze se yachatou ou não?
agf

1
eles são estruturas de diretório, então eu não acho que ele / ela quer algo achatado? oh, desculpe, perdi "vários dicionários". sim, reduzir seria bom. irá adicionar isso.
andrew cooke

Isso faz exatamente o que eu queria! Me desculpe, eu não estava claro o suficiente ... Eu pensei que estava bem com Python, parece que não: - / Eu precisava de uma função recursiva por causa dos dict aninhados, este funciona e eu posso entender :) Eu não parecem ser capazes de fazê-lo funcionar com reduzir embora ...
fdhex

2
Para qualquer pessoa com listas como nível aninhado final ao abrigo da dicts, você pode fazer isso em vez de elevar o erro para concatenar as duas listas: a[key] = a[key] + b[key]. Obrigado pela resposta útil.
Kevinmicke #

1
> se você deseja manter um, pode chamá-lo como mesclagem (dict (a), b) Observe que os dicts aninhados ainda serão modificados. Para evitar isso, use copy.deepcopy.
Rcorre

30

Aqui está uma maneira fácil de fazer isso usando geradores:

def mergedicts(dict1, dict2):
    for k in set(dict1.keys()).union(dict2.keys()):
        if k in dict1 and k in dict2:
            if isinstance(dict1[k], dict) and isinstance(dict2[k], dict):
                yield (k, dict(mergedicts(dict1[k], dict2[k])))
            else:
                # If one of the values is not a dict, you can't continue merging it.
                # Value from second dict overrides one in first and we move on.
                yield (k, dict2[k])
                # Alternatively, replace this with exception raiser to alert you of value conflicts
        elif k in dict1:
            yield (k, dict1[k])
        else:
            yield (k, dict2[k])

dict1 = {1:{"a":"A"},2:{"b":"B"}}
dict2 = {2:{"c":"C"},3:{"d":"D"}}

print dict(mergedicts(dict1,dict2))

Isso imprime:

{1: {'a': 'A'}, 2: {'c': 'C', 'b': 'B'}, 3: {'d': 'D'}}

se você quiser manter o tema gerador você poderia cadeia (dict1.keys (), dict2.keys ())
Andrew Cooke

Isso não receberia chaves duplicadas?
jterrace

Este parece fazer o trabalho, pelo menos no meu conjunto de dados, mas como eu nunca entendi bem o rendimento e os geradores, estou praticamente perdido quanto ao motivo, mas vou me esforçar um pouco mais, pode ser útil!
Fdhex

ah, sim, receberia chaves duplicadas. você ainda precisará envolvê-lo em um conjunto, desculpe.
andrew cooke

2
Achei isso especialmente útil. Mas o mais interessante seria deixar a função resolver os conflitos como parâmetro.
mentatkgs

25

Um problema com essa pergunta é que os valores do ditado podem ser dados arbitrariamente complexos. Com base nessas e em outras respostas, vim com este código:

class YamlReaderError(Exception):
    pass

def data_merge(a, b):
    """merges b into a and return merged result

    NOTE: tuples and arbitrary objects are not handled as it is totally ambiguous what should happen"""
    key = None
    # ## debug output
    # sys.stderr.write("DEBUG: %s to %s\n" %(b,a))
    try:
        if a is None or isinstance(a, str) or isinstance(a, unicode) or isinstance(a, int) or isinstance(a, long) or isinstance(a, float):
            # border case for first run or if a is a primitive
            a = b
        elif isinstance(a, list):
            # lists can be only appended
            if isinstance(b, list):
                # merge lists
                a.extend(b)
            else:
                # append to list
                a.append(b)
        elif isinstance(a, dict):
            # dicts must be merged
            if isinstance(b, dict):
                for key in b:
                    if key in a:
                        a[key] = data_merge(a[key], b[key])
                    else:
                        a[key] = b[key]
            else:
                raise YamlReaderError('Cannot merge non-dict "%s" into dict "%s"' % (b, a))
        else:
            raise YamlReaderError('NOT IMPLEMENTED "%s" into "%s"' % (b, a))
    except TypeError, e:
        raise YamlReaderError('TypeError "%s" in key "%s" when merging "%s" into "%s"' % (e, key, b, a))
    return a

Meu caso de uso está mesclando arquivos YAML, onde só tenho que lidar com um subconjunto de possíveis tipos de dados. Por isso, posso ignorar tuplas e outros objetos. Para mim, uma lógica de mesclagem sensata significa

  • substituir escalares
  • anexar listas
  • mesclar dict adicionando chaves ausentes e atualizando chaves existentes

Tudo o resto e os imprevistos resultam em um erro.


1
Fantástico. Funciona bem em json dumps também. Acabou de remover o tratamento de erros. (Ser preguiçoso, pode fazer mais adequados para json tenho certeza)
dgBP

3
a sequência "isinstance" pode ser substituída por isinstance(a, (str, unicode, int, long, float))não?
simahawk

12

Mesclar dicionários de dicionários

Como esta é a questão canônica (apesar de certas não-generalidades), estou fornecendo a abordagem pitônica canônica para resolver esse problema.

Caso mais simples: "as folhas são dicts aninhados que terminam em dicts vazios":

d1 = {'a': {1: {'foo': {}}, 2: {}}}
d2 = {'a': {1: {}, 2: {'bar': {}}}}
d3 = {'b': {3: {'baz': {}}}}
d4 = {'a': {1: {'quux': {}}}}

Este é o caso mais simples para recursão e eu recomendaria duas abordagens ingênuas:

def rec_merge1(d1, d2):
    '''return new merged dict of dicts'''
    for k, v in d1.items(): # in Python 2, use .iteritems()!
        if k in d2:
            d2[k] = rec_merge1(v, d2[k])
    d3 = d1.copy()
    d3.update(d2)
    return d3

def rec_merge2(d1, d2):
    '''update first dict with second recursively'''
    for k, v in d1.items(): # in Python 2, use .iteritems()!
        if k in d2:
            d2[k] = rec_merge2(v, d2[k])
    d1.update(d2)
    return d1

Acredito que prefiro o segundo ao primeiro, mas lembre-se de que o estado original do primeiro teria que ser reconstruído a partir de sua origem. Aqui está o uso:

>>> from functools import reduce # only required for Python 3.
>>> reduce(rec_merge1, (d1, d2, d3, d4))
{'a': {1: {'quux': {}, 'foo': {}}, 2: {'bar': {}}}, 'b': {3: {'baz': {}}}}
>>> reduce(rec_merge2, (d1, d2, d3, d4))
{'a': {1: {'quux': {}, 'foo': {}}, 2: {'bar': {}}}, 'b': {3: {'baz': {}}}}

Caso complexo: "as folhas são de qualquer outro tipo:"

Portanto, se eles terminam em ditados, é um caso simples de mesclar os ditados vazios finais. Caso contrário, não é tão trivial. Se strings, como você as mescla? Os conjuntos podem ser atualizados da mesma forma, para que possamos dar esse tratamento, mas perdemos a ordem na qual eles foram mesclados. Então, a ordem importa?

Portanto, em vez de mais informações, a abordagem mais simples será fornecer a eles o tratamento padrão de atualização se ambos os valores não forem ditados: ou seja, o valor do segundo ditado substituirá o primeiro, mesmo que o valor do segundo ditado seja Nenhum e o valor do primeiro seja um ditar com muita informação.

d1 = {'a': {1: 'foo', 2: None}}
d2 = {'a': {1: None, 2: 'bar'}}
d3 = {'b': {3: 'baz'}}
d4 = {'a': {1: 'quux'}}

from collections import MutableMapping

def rec_merge(d1, d2):
    '''
    Update two dicts of dicts recursively, 
    if either mapping has leaves that are non-dicts, 
    the second's leaf overwrites the first's.
    '''
    for k, v in d1.items(): # in Python 2, use .iteritems()!
        if k in d2:
            # this next check is the only difference!
            if all(isinstance(e, MutableMapping) for e in (v, d2[k])):
                d2[k] = rec_merge(v, d2[k])
            # we could further check types and merge as appropriate here.
    d3 = d1.copy()
    d3.update(d2)
    return d3

E agora

from functools import reduce
reduce(rec_merge, (d1, d2, d3, d4))

retorna

{'a': {1: 'quux', 2: 'bar'}, 'b': {3: 'baz'}}

Aplicação à pergunta original:

Eu tive que remover as chaves em volta das letras e colocá-las entre aspas simples para que isso seja Python legítimo (caso contrário, elas seriam configuradas literais no Python 2.7+), além de acrescentar uma chave ausente:

dict1 = {1:{"a":'A'}, 2:{"b":'B'}}
dict2 = {2:{"c":'C'}, 3:{"d":'D'}}

e rec_merge(dict1, dict2)agora retorna:

{1: {'a': 'A'}, 2: {'c': 'C', 'b': 'B'}, 3: {'d': 'D'}}

Que corresponde ao resultado desejado da pergunta original (após alterar, por exemplo, o {A}para 'A'.)


10

Com base em @andrew cooke. Esta versão lida com listas aninhadas de dictos e também permite a opção de atualizar os valores

def merge(a, b, path=None, update=True):
    "http://stackoverflow.com/questions/7204805/python-dictionaries-of-dictionaries-merge"
    "merges b into a"
    if path is None: path = []
    for key in b:
        if key in a:
            if isinstance(a[key], dict) and isinstance(b[key], dict):
                merge(a[key], b[key], path + [str(key)])
            elif a[key] == b[key]:
                pass # same leaf value
            elif isinstance(a[key], list) and isinstance(b[key], list):
                for idx, val in enumerate(b[key]):
                    a[key][idx] = merge(a[key][idx], b[key][idx], path + [str(key), str(idx)], update=update)
            elif update:
                a[key] = b[key]
            else:
                raise Exception('Conflict at %s' % '.'.join(path + [str(key)]))
        else:
            a[key] = b[key]
    return a

1
Obrigado, isso é tão útil. Estou tendo listas de ditados em minhas estruturas o tempo todo, as outras soluções não podem mesclar isso adequadamente.
SHernandez 08/09/2015

7

Esse procedimento recursivo simples mescla um dicionário em outro enquanto substitui chaves conflitantes:

#!/usr/bin/env python2.7

def merge_dicts(dict1, dict2):
    """ Recursively merges dict2 into dict1 """
    if not isinstance(dict1, dict) or not isinstance(dict2, dict):
        return dict2
    for k in dict2:
        if k in dict1:
            dict1[k] = merge_dicts(dict1[k], dict2[k])
        else:
            dict1[k] = dict2[k]
    return dict1

print (merge_dicts({1:{"a":"A"}, 2:{"b":"B"}}, {2:{"c":"C"}, 3:{"d":"D"}}))
print (merge_dicts({1:{"a":"A"}, 2:{"b":"B"}}, {1:{"a":"A"}, 2:{"b":"C"}}))

Resultado:

{1: {'a': 'A'}, 2: {'c': 'C', 'b': 'B'}, 3: {'d': 'D'}}
{1: {'a': 'A'}, 2: {'b': 'C'}}

7

Com base nas respostas de @andrew cooke. Ele cuida de listas aninhadas de uma maneira melhor.

def deep_merge_lists(original, incoming):
    """
    Deep merge two lists. Modifies original.
    Recursively call deep merge on each correlated element of list. 
    If item type in both elements are
     a. dict: Call deep_merge_dicts on both values.
     b. list: Recursively call deep_merge_lists on both values.
     c. any other type: Value is overridden.
     d. conflicting types: Value is overridden.

    If length of incoming list is more that of original then extra values are appended.
    """
    common_length = min(len(original), len(incoming))
    for idx in range(common_length):
        if isinstance(original[idx], dict) and isinstance(incoming[idx], dict):
            deep_merge_dicts(original[idx], incoming[idx])

        elif isinstance(original[idx], list) and isinstance(incoming[idx], list):
            deep_merge_lists(original[idx], incoming[idx])

        else:
            original[idx] = incoming[idx]

    for idx in range(common_length, len(incoming)):
        original.append(incoming[idx])


def deep_merge_dicts(original, incoming):
    """
    Deep merge two dictionaries. Modifies original.
    For key conflicts if both values are:
     a. dict: Recursively call deep_merge_dicts on both values.
     b. list: Call deep_merge_lists on both values.
     c. any other type: Value is overridden.
     d. conflicting types: Value is overridden.

    """
    for key in incoming:
        if key in original:
            if isinstance(original[key], dict) and isinstance(incoming[key], dict):
                deep_merge_dicts(original[key], incoming[key])

            elif isinstance(original[key], list) and isinstance(incoming[key], list):
                deep_merge_lists(original[key], incoming[key])

            else:
                original[key] = incoming[key]
        else:
            original[key] = incoming[key]

intuitivo e simétrico. +1 para manipulação lista :)
vdwees

6

Se você tem um nível desconhecido de dicionários, sugiro uma função recursiva:

def combineDicts(dictionary1, dictionary2):
    output = {}
    for item, value in dictionary1.iteritems():
        if dictionary2.has_key(item):
            if isinstance(dictionary2[item], dict):
                output[item] = combineDicts(value, dictionary2.pop(item))
        else:
            output[item] = value
    for item, value in dictionary2.iteritems():
         output[item] = value
    return output

5

Visão geral

A abordagem a seguir subdivide o problema de uma profunda mesclagem de dictos em:

  1. Uma função de mesclagem superficial parametrizada merge(f)(a,b)que usa uma função fpara mesclar dois dictos aeb

  2. Uma função de fusão recursiva fa ser usada junto commerge


Implementação

Uma função para mesclar dois dictos (não aninhados) pode ser escrita de várias maneiras. Eu pessoalmente gosto

def merge(f):
    def merge(a,b): 
        keys = a.keys() | b.keys()
        return {key:f(a.get(key), b.get(key)) for key in keys}
    return merge

Uma boa maneira de definir uma função de fusão recursiva apropriada fé usar multipledispatch, que permite definir funções que avaliam ao longo de caminhos diferentes, dependendo do tipo de argumento.

from multipledispatch import dispatch

#for anything that is not a dict return
@dispatch(object, object)
def f(a, b):
    return b if b is not None else a

#for dicts recurse 
@dispatch(dict, dict)
def f(a,b):
    return merge(f)(a,b)

Exemplo

Para mesclar dois dicts aninhados, basta usar, merge(f)por exemplo:

dict1 = {1:{"a":"A"},2:{"b":"B"}}
dict2 = {2:{"c":"C"},3:{"d":"D"}}
merge(f)(dict1, dict2)
#returns {1: {'a': 'A'}, 2: {'b': 'B', 'c': 'C'}, 3: {'d': 'D'}} 

Notas:

As vantagens dessa abordagem são:

  • A função é criada a partir de funções menores, cada uma fazendo uma única coisa, o que torna o código mais simples de raciocinar e testar

  • O comportamento não é codificado, mas pode ser alterado e estendido conforme necessário, o que melhora a reutilização do código (veja o exemplo abaixo).


Costumização

Algumas respostas também consideram ditados que contêm listas, por exemplo, de outros ditos (potencialmente aninhados). Nesse caso, pode-se querer mapear as listas e mesclá-las com base na posição. Isso pode ser feito adicionando outra definição à função de fusão f:

import itertools
@dispatch(list, list)
def f(a,b):
    return [merge(f)(*arg) for arg in itertools.zip_longest(a, b)]

4

Caso alguém queira mais um abordagem para esse problema, aqui está minha solução.

Virtudes : estilo curto, declarativo e funcional (recursivo, sem mutação).

Desvantagem em potencial : talvez essa não seja a fusão que você está procurando. Consulte a documentação para semântica.

def deep_merge(a, b):
    """
    Merge two values, with `b` taking precedence over `a`.

    Semantics:
    - If either `a` or `b` is not a dictionary, `a` will be returned only if
      `b` is `None`. Otherwise `b` will be returned.
    - If both values are dictionaries, they are merged as follows:
        * Each key that is found only in `a` or only in `b` will be included in
          the output collection with its value intact.
        * For any key in common between `a` and `b`, the corresponding values
          will be merged with the same semantics.
    """
    if not isinstance(a, dict) or not isinstance(b, dict):
        return a if b is None else b
    else:
        # If we're here, both a and b must be dictionaries or subtypes thereof.

        # Compute set of all keys in both dictionaries.
        keys = set(a.keys()) | set(b.keys())

        # Build output dictionary, merging recursively values with common keys,
        # where `None` is used to mean the absence of a value.
        return {
            key: deep_merge(a.get(key), b.get(key))
            for key in keys
        }

Resposta muito interessante, obrigado por compartilhá-lo. Qual sintaxe você usou após a declaração de retorno? Eu não estou familiarizado com isso.
Dev_does_software 16/08/19

4

Você pode tentar a fusão .


Instalação

$ pip3 install mergedeep

Uso

from mergedeep import merge

a = {"keyA": 1}
b = {"keyB": {"sub1": 10}}
c = {"keyB": {"sub2": 20}}

merge(a, b, c) 

print(a)
# {"keyA": 1, "keyB": {"sub1": 10, "sub2": 20}}

Para uma lista completa de opções, consulte os documentos !


3

Há um pequeno problema com a resposta de andrew cookes: Em alguns casos, ele modifica o segundo argumento bquando você modifica o ditado retornado. Especificamente, é por causa dessa linha:

if key in a:
    ...
else:
    a[key] = b[key]

Se b[key]for a dict, ele será simplesmente atribuído a a, o que significa que qualquer modificação subsequente dictafetará ambos ae b.

a={}
b={'1':{'2':'b'}}
c={'1':{'3':'c'}}
merge(merge(a,b), c) # {'1': {'3': 'c', '2': 'b'}}
a # {'1': {'3': 'c', '2': 'b'}} (as expected)
b # {'1': {'3': 'c', '2': 'b'}} <----
c # {'1': {'3': 'c'}} (unmodified)

Para corrigir isso, a linha teria que ser substituída por esta:

if isinstance(b[key], dict):
    a[key] = clone_dict(b[key])
else:
    a[key] = b[key]

Onde clone_dictfica:

def clone_dict(obj):
    clone = {}
    for key, value in obj.iteritems():
        if isinstance(value, dict):
            clone[key] = clone_dict(value)
        else:
            clone[key] = value
    return

Ainda. Obviamente, isso não explica list, sete outras coisas, mas espero que ilustre as armadilhas ao tentar mesclar dicts.

E, para completar, aqui está a minha versão, onde você pode transmiti-la várias dicts:

def merge_dicts(*args):
    def clone_dict(obj):
        clone = {}
        for key, value in obj.iteritems():
            if isinstance(value, dict):
                clone[key] = clone_dict(value)
            else:
                clone[key] = value
        return

    def merge(a, b, path=[]):
        for key in b:
            if key in a:
                if isinstance(a[key], dict) and isinstance(b[key], dict):
                    merge(a[key], b[key], path + [str(key)])
                elif a[key] == b[key]:
                    pass
                else:
                    raise Exception('Conflict at `{path}\''.format(path='.'.join(path + [str(key)])))
            else:
                if isinstance(b[key], dict):
                    a[key] = clone_dict(b[key])
                else:
                    a[key] = b[key]
        return a
    return reduce(merge, args, {})

Por que não em deepcopyvez de clone_dict?
Armando Pérez Marqués

1
Porque o python stdlib é enorme e magnífico! Eu não tinha idéia isso existia - além disso, era um divertimento pouca coisa a código :-)
andsens

2

Esta versão da função contabilizará N número de dicionários e apenas dicionários - nenhum parâmetro impróprio pode ser passado ou gerará um TypeError. A mesclagem em si é responsável por conflitos-chave e, em vez de sobrescrever dados de um dicionário mais abaixo na cadeia de mesclagem, cria um conjunto de valores e anexa a ele; nenhum dado é perdido.

Pode não ser o mais eficiente da página, mas é o mais completo e você não perderá nenhuma informação ao mesclar seus ditados 2 a N.

def merge_dicts(*dicts):
    if not reduce(lambda x, y: isinstance(y, dict) and x, dicts, True):
        raise TypeError, "Object in *dicts not of type dict"
    if len(dicts) < 2:
        raise ValueError, "Requires 2 or more dict objects"


    def merge(a, b):
        for d in set(a.keys()).union(b.keys()):
            if d in a and d in b:
                if type(a[d]) == type(b[d]):
                    if not isinstance(a[d], dict):
                        ret = list({a[d], b[d]})
                        if len(ret) == 1: ret = ret[0]
                        yield (d, sorted(ret))
                    else:
                        yield (d, dict(merge(a[d], b[d])))
                else:
                    raise TypeError, "Conflicting key:value type assignment"
            elif d in a:
                yield (d, a[d])
            elif d in b:
                yield (d, b[d])
            else:
                raise KeyError

    return reduce(lambda x, y: dict(merge(x, y)), dicts[1:], dicts[0])

print merge_dicts({1:1,2:{1:2}},{1:2,2:{3:1}},{4:4})

saída: {1: [1, 2], 2: {1: 2, 3: 1}, 4: 4}


2

Como o dictviews suporta operações de conjunto, pude simplificar bastante a resposta do jterrace.

def merge(dict1, dict2):
    for k in dict1.keys() - dict2.keys():
        yield (k, dict1[k])

    for k in dict2.keys() - dict1.keys():
        yield (k, dict2[k])

    for k in dict1.keys() & dict2.keys():
        yield (k, dict(merge(dict1[k], dict2[k])))

Qualquer tentativa de combinar um dict com um não dict (tecnicamente, um objeto com o método 'keys' e um objeto sem o método 'keys') gerará um AttributeError. Isso inclui tanto a chamada inicial para a função quanto as chamadas recursivas. Era exatamente isso que eu queria, então deixei. Você pode capturar facilmente um AttributeErrors emitido pela chamada recursiva e depois gerar qualquer valor que desejar.


2

Curto e doce:

from collections.abc import MutableMapping as Map

def nested_update(d, v):
"""
Nested update of dict-like 'd' with dict-like 'v'.
"""

for key in v:
    if key in d and isinstance(d[key], Map) and isinstance(v[key], Map):
        nested_update(d[key], v[key])
    else:
        d[key] = v[key]

Isso funciona como (e é construído sobre) o dict.updatemétodo Python . Ele retorna None(você sempre pode adicionar, return dse preferir), pois atualiza o ditado dno local. A inserção de chaves vsubstituirá as chaves existentes emd (não tenta interpretar o conteúdo do ditado).

Também funcionará para outros mapeamentos ("dict-like").


1

O código dependerá das suas regras para resolver conflitos de mesclagem, é claro. Aqui está uma versão que pode pegar um número arbitrário de argumentos e mesclá-los recursivamente a uma profundidade arbitrária, sem usar nenhuma mutação de objeto. Ele usa as seguintes regras para resolver conflitos de mesclagem:

  • os dicionários têm precedência sobre valores não-dict ( {"foo": {...}}tem precedência sobre{"foo": "bar"} )
  • argumentos posteriores têm precedência sobre argumentos anteriores (se você mesclar {"a": 1}, {"a", 2}e {"a": 3}em ordem, o resultado será {"a": 3})
try:
    from collections import Mapping
except ImportError:
    Mapping = dict

def merge_dicts(*dicts):                                                            
    """                                                                             
    Return a new dictionary that is the result of merging the arguments together.   
    In case of conflicts, later arguments take precedence over earlier arguments.   
    """                                                                             
    updated = {}                                                                    
    # grab all keys                                                                 
    keys = set()                                                                    
    for d in dicts:                                                                 
        keys = keys.union(set(d))                                                   

    for key in keys:                                                                
        values = [d[key] for d in dicts if key in d]                                
        # which ones are mapping types? (aka dict)                                  
        maps = [value for value in values if isinstance(value, Mapping)]            
        if maps:                                                                    
            # if we have any mapping types, call recursively to merge them          
            updated[key] = merge_dicts(*maps)                                       
        else:                                                                       
            # otherwise, just grab the last value we have, since later arguments    
            # take precedence over earlier arguments                                
            updated[key] = values[-1]                                               
    return updated  

1

Eu tinha dois dicionários ( ae b) que cada um poderia conter qualquer número de dicionários aninhados. Eu queria mesclá-los recursivamente, com bprecedência a.

Considerando os dicionários aninhados como árvores, o que eu queria era:

  • Atualizar apara que todo caminho para cada folha bseja representado ema
  • Para sobrescrever subárvores de ase uma folha for encontrada no caminho correspondente emb
    • Mantenha a invariante de que todos os bnós das folhas permanecem folhas.

As respostas existentes foram um pouco complicadas para o meu gosto e deixaram alguns detalhes na prateleira. Eu hackeei o seguinte, que passa nos testes de unidade do meu conjunto de dados.

  def merge_map(a, b):
    if not isinstance(a, dict) or not isinstance(b, dict):
      return b

    for key in b.keys():
      a[key] = merge_map(a[key], b[key]) if key in a else b[key]
    return a

Exemplo (formatado para maior clareza):

 a = {
    1 : {'a': 'red', 
         'b': {'blue': 'fish', 'yellow': 'bear' },
         'c': { 'orange': 'dog'},
    },
    2 : {'d': 'green'},
    3: 'e'
  }

  b = {
    1 : {'b': 'white'},
    2 : {'d': 'black'},
    3: 'e'
  }


  >>> merge_map(a, b)
  {1: {'a': 'red', 
       'b': 'white',
       'c': {'orange': 'dog'},},
   2: {'d': 'black'},
   3: 'e'}

Os caminhos bque precisavam ser mantidos foram:

  • 1 -> 'b' -> 'white'
  • 2 -> 'd' -> 'black'
  • 3 -> 'e'.

a tinha os caminhos únicos e não conflitantes de:

  • 1 -> 'a' -> 'red'
  • 1 -> 'c' -> 'orange' -> 'dog'

para que eles ainda sejam representados no mapa mesclado.


1

Eu tenho uma solução iterativa - funciona muito melhor com dictos grandes e muitos deles (por exemplo, jsons etc):

import collections


def merge_dict_with_subdicts(dict1: dict, dict2: dict) -> dict:
    """
    similar behaviour to builtin dict.update - but knows how to handle nested dicts
    """
    q = collections.deque([(dict1, dict2)])
    while len(q) > 0:
        d1, d2 = q.pop()
        for k, v in d2.items():
            if k in d1 and isinstance(d1[k], dict) and isinstance(v, dict):
                q.append((d1[k], v))
            else:
                d1[k] = v

    return dict1

observe que isso usará o valor em d2 para substituir d1, caso eles não sejam os dois dict. (igual ao de pythondict.update() )

alguns testes:

def test_deep_update():
    d = dict()
    merge_dict_with_subdicts(d, {"a": 4})
    assert d == {"a": 4}

    new_dict = {
        "b": {
            "c": {
                "d": 6
            }
        }
    }
    merge_dict_with_subdicts(d, new_dict)
    assert d == {
        "a": 4,
        "b": {
            "c": {
                "d": 6
            }
        }
    }

    new_dict = {
        "a": 3,
        "b": {
            "f": 7
        }
    }
    merge_dict_with_subdicts(d, new_dict)
    assert d == {
        "a": 3,
        "b": {
            "c": {
                "d": 6
            },
            "f": 7
        }
    }

    # test a case where one of the dicts has dict as value and the other has something else
    new_dict = {
        'a': {
            'b': 4
        }
    }
    merge_dict_with_subdicts(d, new_dict)
    assert d['a']['b'] == 4

Eu testei com cerca de ~ 1200 dictos - esse método levou 0,4 segundos, enquanto a solução recursiva levou ~ 2,5 segundos.


0

Isso deve ajudar na fusão todos os itens de dict2dentro dict1:

for item in dict2:
    if item in dict1:
        for leaf in dict2[item]:
            dict1[item][leaf] = dict2[item][leaf]
    else:
        dict1[item] = dict2[item]

Teste-o e diga-nos se é isso que você queria.

EDITAR:

A solução acima mencionada mescla apenas um nível, mas resolve corretamente o exemplo dado pelo OP. Para mesclar vários níveis, a recursão deve ser usada.


1
Ele tem uma profundidade arbitrária de aninhamento
agf

Isso pode ser reescrito simplesmente como for k,v in dict2.iteritems(): dict1.setdefault(k,{}).update(v). Mas como o @agf apontou, isso não mescla os dicts aninhados.
Shawn Chin

@agf: Correto, pelo que parece, o OP precisa de solução empregando recorrência. Graças ao fato dos dicionários serem mutáveis, isso deve ser bastante fácil de ser feito. Mas eu acho que a questão não é suficientemente específico para dizer o que deve acontecer quando chegar a lugares com diferentes níveis de profundidade (ex. Tentando mesclar {'a':'b'}com {'a':{'c':'d'}).
Tadeck

0

Venho testando suas soluções e decidi usar esta no meu projeto:

def mergedicts(dict1, dict2, conflict, no_conflict):
    for k in set(dict1.keys()).union(dict2.keys()):
        if k in dict1 and k in dict2:
            yield (k, conflict(dict1[k], dict2[k]))
        elif k in dict1:
            yield (k, no_conflict(dict1[k]))
        else:
            yield (k, no_conflict(dict2[k]))

dict1 = {1:{"a":"A"}, 2:{"b":"B"}}
dict2 = {2:{"c":"C"}, 3:{"d":"D"}}

#this helper function allows for recursion and the use of reduce
def f2(x, y):
    return dict(mergedicts(x, y, f2, lambda x: x))

print dict(mergedicts(dict1, dict2, f2, lambda x: x))
print dict(reduce(f2, [dict1, dict2]))

Passar funções como parâmetros é a chave para estender a solução jterrace para se comportar como todas as outras soluções recursivas.


0

A maneira mais fácil de pensar é:

#!/usr/bin/python

from copy import deepcopy
def dict_merge(a, b):
    if not isinstance(b, dict):
        return b
    result = deepcopy(a)
    for k, v in b.iteritems():
        if k in result and isinstance(result[k], dict):
                result[k] = dict_merge(result[k], v)
        else:
            result[k] = deepcopy(v)
    return result

a = {1:{"a":'A'}, 2:{"b":'B'}}
b = {2:{"c":'C'}, 3:{"d":'D'}}

print dict_merge(a,b)

Resultado:

{1: {'a': 'A'}, 2: {'c': 'C', 'b': 'B'}, 3: {'d': 'D'}}

0

Eu tenho outra solução um pouco diferente aqui:

def deepMerge(d1, d2, inconflict = lambda v1,v2 : v2) :
''' merge d2 into d1. using inconflict function to resolve the leaf conflicts '''
    for k in d2:
        if k in d1 : 
            if isinstance(d1[k], dict) and isinstance(d2[k], dict) :
                deepMerge(d1[k], d2[k], inconflict)
            elif d1[k] != d2[k] :
                d1[k] = inconflict(d1[k], d2[k])
        else :
            d1[k] = d2[k]
    return d1

Por padrão, ele resolve conflitos em favor dos valores do segundo ditado, mas você pode facilmente substituí-lo; com algumas bruxarias, você pode até lançar exceções nele. :).


0
class Utils(object):

    """

    >>> a = { 'first' : { 'all_rows' : { 'pass' : 'dog', 'number' : '1' } } }
    >>> b = { 'first' : { 'all_rows' : { 'fail' : 'cat', 'number' : '5' } } }
    >>> Utils.merge_dict(b, a) == { 'first' : { 'all_rows' : { 'pass' : 'dog', 'fail' : 'cat', 'number' : '5' } } }
    True

    >>> main = {'a': {'b': {'test': 'bug'}, 'c': 'C'}}
    >>> suply = {'a': {'b': 2, 'd': 'D', 'c': {'test': 'bug2'}}}
    >>> Utils.merge_dict(main, suply) == {'a': {'b': {'test': 'bug'}, 'c': 'C', 'd': 'D'}}
    True

    """

    @staticmethod
    def merge_dict(main, suply):
        """
        获取融合的字典,以main为主,suply补充,冲突时以main为准
        :return:
        """
        for key, value in suply.items():
            if key in main:
                if isinstance(main[key], dict):
                    if isinstance(value, dict):
                        Utils.merge_dict(main[key], value)
                    else:
                        pass
                else:
                    pass
            else:
                main[key] = value
        return main

if __name__ == '__main__':
    import doctest
    doctest.testmod()

0

ei ai, eu também tive o mesmo problema, mas pensei em uma solução e vou publicá-la aqui, caso também seja útil para outras pessoas, basicamente mesclando dicionários aninhados e também adicionando valores, para mim eu precisava calcular algumas probabilidades para que isso um funcionou muito bem:

#used to copy a nested dict to a nested dict
def deepupdate(target, src):
    for k, v in src.items():
        if k in target:
            for k2, v2 in src[k].items():
                if k2 in target[k]:
                    target[k][k2]+=v2
                else:
                    target[k][k2] = v2
        else:
            target[k] = copy.deepcopy(v)

usando o método acima, podemos mesclar:

target = {'6,6': {'6,63': 1}, '63, 4 ': {' 4,4 ': 1},' 4,4 ': {' 4,3 ': 1} , '6,63': {'63, 4 ': 1}}

src = {'5,4': {'4,4': 1}, '5,5': {'5,4': 1}, '4,4': {'4,3': 1} }

e isso se tornará: {'5,5': {'5,4': 1}, '5,4': {'4,4': 1}, '6,6': {'6,63' : 1}, '63, 4 ': {' 4,4 ': 1},' 4,4 ': {' 4,3 ': 2},' 6,63 ': {'63, 4': 1 }}

observe também as alterações aqui:

target = {'6,6': {'6,63': 1}, '6,63': {'63, 4 ': 1}, ' 4,4 ': {' 4,3 ': 1} , '63, 4 ': {' 4,4 ': 1}}

src = {'5,4': {'4,4': 1}, '4,3': {'3,4': 1}, '4,4': {'4,9': 1} , '3,4': {'4,4': 1}, '5,5': {'5,4': 1}}

mescla = {'5,4': {'4,4': 1}, '4,3': {'3,4': 1}, '6,63': {'63, 4 ': 1} , '5,5': {'5,4': 1}, '6,6': {'6,63': 1}, '3,4': {'4,4': 1}, ' 63,4 ': {' 4,4 ': 1}, ' 4,4 ': {' 4,3 ': 1,' 4,9 ': 1} }

não se esqueça de adicionar também a importação para cópia:

import copy

0
from collections import defaultdict
from itertools import chain

class DictHelper:

@staticmethod
def merge_dictionaries(*dictionaries, override=True):
    merged_dict = defaultdict(set)
    all_unique_keys = set(chain(*[list(dictionary.keys()) for dictionary in dictionaries]))  # Build a set using all dict keys
    for key in all_unique_keys:
        keys_value_type = list(set(filter(lambda obj_type: obj_type != type(None), [type(dictionary.get(key, None)) for dictionary in dictionaries])))
        # Establish the object type for each key, return None if key is not present in dict and remove None from final result
        if len(keys_value_type) != 1:
            raise Exception("Different objects type for same key: {keys_value_type}".format(keys_value_type=keys_value_type))

        if keys_value_type[0] == list:
            values = list(chain(*[dictionary.get(key, []) for dictionary in dictionaries]))  # Extract the value for each key
            merged_dict[key].update(values)

        elif keys_value_type[0] == dict:
            # Extract all dictionaries by key and enter in recursion
            dicts_to_merge = list(filter(lambda obj: obj != None, [dictionary.get(key, None) for dictionary in dictionaries]))
            merged_dict[key] = DictHelper.merge_dictionaries(*dicts_to_merge)

        else:
            # if override => get value from last dictionary else make a list of all values
            values = list(filter(lambda obj: obj != None, [dictionary.get(key, None) for dictionary in dictionaries]))
            merged_dict[key] = values[-1] if override else values

    return dict(merged_dict)



if __name__ == '__main__':
  d1 = {'aaaaaaaaa': ['to short', 'to long'], 'bbbbb': ['to short', 'to long'], "cccccc": ["the is a test"]}
  d2 = {'aaaaaaaaa': ['field is not a bool'], 'bbbbb': ['field is not a bool']}
  d3 = {'aaaaaaaaa': ['filed is not a string', "to short"], 'bbbbb': ['field is not an integer']}
  print(DictHelper.merge_dictionaries(d1, d2, d3))

  d4 = {"a": {"x": 1, "y": 2, "z": 3, "d": {"x1": 10}}}
  d5 = {"a": {"x": 10, "y": 20, "d": {"x2": 20}}}
  print(DictHelper.merge_dictionaries(d4, d5))

Resultado:

{'bbbbb': {'to long', 'field is not an integer', 'to short', 'field is not a bool'}, 
'aaaaaaaaa': {'to long', 'to short', 'filed is not a string', 'field is not a bool'}, 
'cccccc': {'the is a test'}}

{'a': {'y': 20, 'd': {'x1': 10, 'x2': 20}, 'z': 3, 'x': 10}}

Embora esse código possa responder à pergunta, fornecer um contexto adicional a respeito de por que e / ou como esse código responde à pergunta melhora seu valor a longo prazo.
Xiawi 11/09/19

Eu acho que essa é uma implementação genérica de fundir um ou dicionários mais aninhados tendo em consideração o tipo de objetos que serão Marged
Dorcioman

0

dê uma olhada no toolzpacote

import toolz
dict1={1:{"a":"A"},2:{"b":"B"}}
dict2={2:{"c":"C"},3:{"d":"D"}}
toolz.merge_with(toolz.merge,dict1,dict2)

{1: {'a': 'A'}, 2: {'b': 'B', 'c': 'C'}, 3: {'d': 'D'}}

0

A função a seguir mescla b em a.

def mergedicts(a, b):
    for key in b:
        if isinstance(a.get(key), dict) or isinstance(b.get(key), dict):
            mergedicts(a[key], b[key])
        else:
            a[key] = b[key]
    return a

0

E apenas outra ligeira variação:

Aqui está uma função de atualização profunda baseada em conjunto python3 pura. Ele atualiza dicionários aninhados percorrendo um nível de cada vez e se chama para atualizar cada próximo nível de valores do dicionário:

def deep_update(dict_original, dict_update):
    if isinstance(dict_original, dict) and isinstance(dict_update, dict):
        output=dict(dict_original)
        keys_original=set(dict_original.keys())
        keys_update=set(dict_update.keys())
        similar_keys=keys_original.intersection(keys_update)
        similar_dict={key:deep_update(dict_original[key], dict_update[key]) for key in similar_keys}
        new_keys=keys_update.difference(keys_original)
        new_dict={key:dict_update[key] for key in new_keys}
        output.update(similar_dict)
        output.update(new_dict)
        return output
    else:
        return dict_update

Um exemplo simples:

x={'a':{'b':{'c':1, 'd':1}}}
y={'a':{'b':{'d':2, 'e':2}}, 'f':2}

print(deep_update(x, y))
>>> {'a': {'b': {'c': 1, 'd': 2, 'e': 2}}, 'f': 2}

0

Que tal outra resposta?!? Este também evita mutação / efeitos colaterais:

def merge(dict1, dict2):
    output = {}

    # adds keys from `dict1` if they do not exist in `dict2` and vice-versa
    intersection = {**dict2, **dict1}

    for k_intersect, v_intersect in intersection.items():
        if k_intersect not in dict1:
            v_dict2 = dict2[k_intersect]
            output[k_intersect] = v_dict2

        elif k_intersect not in dict2:
            output[k_intersect] = v_intersect

        elif isinstance(v_intersect, dict):
            v_dict2 = dict2[k_intersect]
            output[k_intersect] = merge(v_intersect, v_dict2)

        else:
            output[k_intersect] = v_intersect

    return output
dict1 = {1:{"a":{"A"}}, 2:{"b":{"B"}}}
dict2 = {2:{"c":{"C"}}, 3:{"d":{"D"}}}
dict3 = {1:{"a":{"A"}}, 2:{"b":{"B"},"c":{"C"}}, 3:{"d":{"D"}}}

assert dict3 == merge(dict1, dict2)
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.