¿Cómo fusionar diccionarios de diccionarios?


129

Necesito fusionar varios diccionarios, esto es lo que tengo, por ejemplo:

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

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

Con A B Cy Dsiendo hojas del árbol, como{"info1":"value", "info2":"value2"}

Hay un nivel desconocido (profundidad) de diccionarios, podría ser {2:{"c":{"z":{"y":{C}}}}}

En mi caso, representa una estructura de directorio / archivos con nodos que son documentos y deja que sean archivos.

Quiero fusionarlos para obtener:

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

No estoy seguro de cómo podría hacer eso fácilmente con Python.


¿Qué quieres para tu profundidad arbitraria de diccionarios? ¿Quieres yaplanar hasta el cnivel o qué? Tu ejemplo está incompleto.
agf

Verifique mi clase NestedDict aquí: stackoverflow.com/a/16296144/2334951 Realiza la gestión de estructuras de diccionario anidadas como la fusión y más.
Szieberth Adam

3
Una advertencia para todos los que buscan soluciones: esta pregunta se trata solo de dictados anidados. La mayoría de las respuestas no manejan el caso más complicado de listas de dictados dentro de la estructura correctamente. Si necesita esto, pruebe la respuesta de @Osiloke a continuación: stackoverflow.com/a/25270947/1431660
SHernandez


Respuestas:


143

esto es realmente bastante complicado, especialmente si desea un mensaje de error útil cuando las cosas son inconsistentes, mientras acepta correctamente entradas duplicadas pero consistentes (algo que ninguna otra respuesta aquí ...)

suponiendo que no tenga un gran número de entradas, una función recursiva es más 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"}})

tenga en cuenta que esto muta a: el contenido de bse agrega a a(que también se devuelve). si quieres quedarte apuedes llamarlo así merge(dict(a), b).

agf señaló (a continuación) que puede tener más de dos dictados, en cuyo caso puede usar:

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

donde todo se agregará a dict1.

[nota: edité mi respuesta inicial para mutar el primer argumento; eso hace que el "reducir" sea más fácil de explicar]

ps en python 3, también necesitarás from functools import reduce


1
Luego puede pegar esto dentro de un reducebucle o el equivalente para trabajar con un número arbitrario de dicts en lugar de dos. Sin embargo, no estoy seguro de que esto hace lo que quiere, ya sea (que no estaba clara), Se termina con 2: {'c': {'z': {'y': {'info1': 'value', 'info2': 'value2'}}}, 'b': {'info1': 'value', 'info2': 'value2'}}su segundo ejemplo, no estoy seguro de si él quiere que el zy yaplanado o no?
AGF

1
son estructuras de directorio, así que no creo que quiera algo aplanado. oh, perdón, perdí "múltiples diccionarios". Sí, reducir sería bueno. agregará eso.
Andrew Cooke

¡Esto hace exactamente lo que quería! Lo siento, no estaba lo suficientemente claro ... Pensé que estaba bien con Python, parece que no: - / Necesitaba una función recursiva debido a los dictados anidados, este funciona y puedo entenderlo :) No lo sé parece ser capaz de hacerlo funcionar con reducir sin embargo ...
fdhex

2
Para cualquier persona con listas como el nivel anidado final bajo las dicts, usted puede hacer esto en lugar de elevar el error para concatenar las dos listas: a[key] = a[key] + b[key]. Gracias por la útil respuesta.
kevinmicke

1
> si desea conservar un, podría llamarlo como merge (dict (a), b) Tenga en cuenta que los dictados anidados seguirán mutados. Para evitar esto, use copy.deepcopy.
rcorre

31

Aquí hay una manera fácil de hacerlo usando generadores:

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

Esto imprime:

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

si desea mantener el tema del generador, puede encadenar (dict1.keys (), dict2.keys ())
andrew cooke

¿No obtendría eso claves duplicadas?
jterrace

Este parece hacer el trabajo, al menos en mi conjunto de datos, pero como nunca entendí bien el rendimiento y los generadores, estoy bastante perdido en cuanto a por qué, pero voy a intentarlo un poco más, ¡podría ser útil!
fdhex

ah, sí, obtendría claves duplicadas. aún necesitarías envolverlo en un conjunto, lo siento.
Andrew Cooke

2
Encontré esto especialmente útil. Pero lo interesante sería dejar que la función resuelva los conflictos como parámetro.
mentatkgs

25

Un problema con esta pregunta es que los valores del dict pueden ser datos arbitrariamente complejos. En base a estas y otras respuestas, se me ocurrió 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

Mi caso de uso es fusionar archivos YAML donde solo tengo que lidiar con un subconjunto de tipos de datos posibles. Por lo tanto, puedo ignorar las tuplas y otros objetos. Para mí, una lógica de fusión sensata significa

  • reemplazar escalares
  • agregar listas
  • fusionar dictos agregando claves faltantes y actualizando claves existentes

Todo lo demás y los imprevistos resultan en un error.


1
Fantástico. Funciona bien en json dumps, también. Acabo de eliminar el manejo de errores. (Ser perezoso, puede hacer las apropiadas para JSON estoy seguro)
dgBP

3
la secuencia "isinstance" se puede reemplazar con isinstance(a, (str, unicode, int, long, float))isnt 'it?
simahawk

12

Diccionarios de diccionarios se fusionan

Como esta es la pregunta canónica (a pesar de ciertas no generalidades), estoy proporcionando el enfoque canónico Pythonic para resolver este problema.

Caso más simple: "las hojas son dictados anidados que terminan en dictos vacíos":

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

Este es el caso más simple para la recursividad, y recomendaría dos enfoques ingenuos:

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

Creo que preferiría el segundo al primero, pero tenga en cuenta que el estado original del primero tendría que ser reconstruido desde su origen. Aquí está el 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 complejo: "las hojas son de cualquier otro tipo:"

Entonces, si terminan en dictados, es un caso simple de fusionar los dictados vacíos finales. Si no, no es tan trivial. Si son cadenas, ¿cómo las fusionas? Los conjuntos se pueden actualizar de manera similar, por lo que podríamos dar ese tratamiento, pero perdemos el orden en que se fusionaron. Entonces, ¿importa el orden?

Entonces, en lugar de más información, el enfoque más simple será darles el tratamiento de actualización estándar si ambos valores no son dictados: es decir, el valor del segundo dict sobrescribirá al primero, incluso si el valor del segundo dict es None y el valor del primero es un dict con mucha información.

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

Y ahora

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

devoluciones

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

Aplicación a la pregunta original:

He tenido que eliminar las llaves alrededor de las letras y ponerlas entre comillas para que sea Python legítimo (de lo contrario, se establecerían literales en Python 2.7+), así como agregar una llave faltante:

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

y rec_merge(dict1, dict2)ahora vuelve:

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

Que coincide con el resultado deseado de la pregunta original (después de cambiar, por ejemplo, el {A}para 'A').


10

Basado en @andrew cooke. Esta versión maneja listas anidadas de dictados y también permite la opción de actualizar los 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
Gracias, esto es muy útil. Tengo listas de dictados en mis estructuras todo el tiempo, las otras soluciones no pueden combinar esto correctamente.
SHernandez

7

Este simple procedimiento recursivo fusionará un diccionario en otro mientras anula las claves en conflicto:

#!/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"}}))

Salida:

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

7

Basado en las respuestas de @andrew cooke. Se encarga de las listas anidadas de una mejor manera.

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 y simétrico. +1 para el manejo de la lista :)
vdwees

6

Si tiene un nivel desconocido de diccionarios, sugeriría una función 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

Visión general

El siguiente enfoque subdivide el problema de una profunda fusión de dictos en:

  1. Una función de fusión superficial parametrizada merge(f)(a,b)que utiliza una función fpara fusionar dos dictados ayb

  2. Una función de fusión recursiva fpara ser utilizada junto conmerge


Implementación

Una función para fusionar dos dictados (no anidados) se puede escribir de muchas maneras. Personalmente me gusta

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

Una buena manera de definir una función de fusión recursiva adecuada fes usar multipledispatch que permite definir funciones que evalúan a lo largo de diferentes rutas dependiendo del tipo de sus argumentos.

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)

Ejemplo

Para fusionar dos dictados anidados, simplemente use, merge(f)por ejemplo:

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:

Las ventajas de este enfoque son:

  • La función se construye a partir de funciones más pequeñas que hacen una sola cosa, lo que hace que el código sea más simple para razonar y probar

  • El comportamiento no está codificado, pero puede cambiarse y ampliarse según sea necesario, lo que mejora la reutilización del código (consulte el ejemplo a continuación).


Personalización

Algunas respuestas también consideraron dictados que contienen listas, por ejemplo, de otros dictados (potencialmente anidados). En este caso, es posible que desee asignar un mapa sobre las listas y fusionarlas según la posición. Esto se puede hacer agregando otra definición a la función de fusión f:

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

4

En caso de que alguien quiera otro enfoque para este problema, aquí está mi solución.

Virtudes : estilo corto, declarativo y funcional (recursivo, sin mutación).

Posible inconveniente : esta podría no ser la fusión que estás buscando. Consulte la cadena de documentación 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
        }

Respuesta muy interesante, gracias por compartirlo. ¿Qué sintaxis usaste después de la declaración de devolución? No estoy familiarizado con eso.
dev_does_software

4

Podrías intentar fusionar en profundidad .


Instalación

$ 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 obtener una lista completa de opciones, ¡ mira los documentos !


3

Hay un pequeño problema con la respuesta de Andrew Cookes: en algunos casos modifica el segundo argumento bcuando modifica el dict devuelto. Específicamente es por esta línea:

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

Si b[key]es a dict, simplemente se le asignará a, lo que significa que cualquier modificación posterior a eso dictafectará a ambos ay 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 arreglar esto, la línea tendría que ser sustituida por esto:

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

Donde clone_dictes:

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

Todavía. Obviamente, esto no tiene en cuenta list, sety otras cosas, pero espero que ilustre las trampas al intentar fusionarse dicts.

Y para completar, aquí está mi versión, donde puedes pasarla múltiple 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 qué no en deepcopylugar de clone_dict?
Armando Pérez Marqués

1
¡Porque el python stdlib es enorme y magnífico! No tenía idea de que esto existía, además de que era una cosa divertida de codificar :-)
andsens

2

Esta versión de la función tendrá en cuenta N número de diccionarios, y solo diccionarios: no se pueden pasar parámetros incorrectos, o generará un TypeError. La fusión en sí tiene en cuenta los conflictos clave y, en lugar de sobrescribir los datos de un diccionario más abajo en la cadena de fusión, crea un conjunto de valores y se agrega a eso; No se pierden datos.

Puede que no sea el más eficiente en la página, pero es el más completo y no perderá ninguna información cuando combine sus dictados de 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})

salida: {1: [1, 2], 2: {1: 2, 3: 1}, 4: 4}


2

Dado que dictviews admite operaciones de conjunto, pude simplificar enormemente la respuesta de 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])))

Cualquier intento de combinar un dict con un no dict (técnicamente, un objeto con un método de 'claves' y un objeto sin un método de 'claves') generará un AttributeError. Esto incluye tanto la llamada inicial a la función como las llamadas recursivas. Esto es exactamente lo que quería, así que lo dejé. Puede atrapar fácilmente un AttributeErrors arrojado por la llamada recursiva y luego obtener el valor que desee.


2

Corto y dulce:

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]

Esto funciona como (y se basa en) el dict.updatemétodo de Python . Regresa None(siempre puede agregar return dsi lo prefiere) a medida que actualiza dict din situ. Keys in vsobrescribirá cualquier clave existente end (no intenta interpretar el contenido del dict).

También funcionará para otras asignaciones ("dict-like").


1

El código dependerá de sus reglas para resolver conflictos de fusión, por supuesto. Aquí hay una versión que puede tomar un número arbitrario de argumentos y fusionarlos recursivamente a una profundidad arbitraria, sin usar ninguna mutación de objeto. Utiliza las siguientes reglas para resolver conflictos de fusión:

  • los diccionarios tienen prioridad sobre los valores no dictados ( {"foo": {...}}tiene prioridad sobre{"foo": "bar"} )
  • argumentos posteriores tienen prioridad sobre los argumentos anteriores (si se fusionan {"a": 1}, {"a", 2}y {"a": 3}el fin, el 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

Tenía dos diccionarios ( ay b) que podían contener cada uno cualquier número de diccionarios anidados. Quería fusionarlos recursivamente, con bprioridad sobrea .

Considerando los diccionarios anidados como árboles, lo que quería era:

  • Para actualizar apara que cada camino a cada hoja enb esté representada ena
  • Para sobrescribir subárboles de asi se encuentra una hoja en la ruta correspondiente enb
    • Mantenga la invariante de que todos blos nodos de las hojas siguen siendo hojas.

Las respuestas existentes fueron un poco complicadas para mi gusto y dejaron algunos detalles en el estante. Pirateé juntos lo siguiente, que pasa las pruebas unitarias para mi conjunto de datos.

  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

Ejemplo (formateado para mayor claridad):

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

Los caminos bque necesitaban mantenerse eran:

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

a tenía las rutas únicas y no conflictivas de:

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

así que todavía están representados en el mapa combinado.


1

Tengo una solución iterativa: funciona mucho mejor con grandes dictados y muchos de ellos (por ejemplo, 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

tenga en cuenta que esto usará el valor en d2 para anular d1, en caso de que no sean ambos dictados. (igual que el de pitóndict.update() )

algunas pruebas:

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

He probado con alrededor de ~ 1200 dictos: este método tomó 0.4 segundos, mientras que la solución recursiva tomó ~ 2.5 segundos.


0

Esto debería ayudar a fusionar todos los elementos de dict2en 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]

Por favor, pruébelo y díganos si esto es lo que quería.

EDITAR:

La solución mencionada anteriormente combina solo un nivel, pero resuelve correctamente el ejemplo dado por OP. Para fusionar múltiples niveles, se debe utilizar la recursividad.


1
Tiene una profundidad arbitraria de anidación
agf

Eso se puede reescribir simplemente como for k,v in dict2.iteritems(): dict1.setdefault(k,{}).update(v). Pero como señaló @agf, esto no combina los dictados anidados.
Shawn Chin

@agf: Correcto, por lo que parece que OP necesita una solución que recurra. Gracias al hecho de que los diccionarios son mutables, esto debería ser bastante fácil de hacer. Pero creo que la pregunta no es lo suficientemente específico para decir lo que debe suceder cuando nos encontramos con lugares con diferentes niveles de profundidad (por ejemplo. Tratando de fusionarse {'a':'b'}con {'a':{'c':'d'}).
Tadeck

0

He estado probando sus soluciones y decidí usar esta en mi proyecto:

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

Pasar funciones como parámetros es clave para extender la solución jterrace para que se comporte como todas las demás soluciones recursivas.


0

La forma más fácil que puedo pensar es:

#!/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)

Salida:

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

0

Tengo otra solución ligeramente diferente aquí:

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

De forma predeterminada, resuelve conflictos a favor de los valores del segundo dict, pero puede anular esto fácilmente, con algo de brujería puede incluso arrojar excepciones. :).


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

Hola, también tuve el mismo problema, pero pensé en una solución y lo publicaré aquí, en caso de que también sea útil para otros, básicamente fusionando diccionarios anidados y también agregando los valores, para mí necesitaba calcular algunas probabilidades para que esto uno funcionó muy bien:

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

Al utilizar el método anterior podemos fusionar:

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

y esto se convertirá en: {'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 }}

También observe los cambios aquí:

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

fusionar = {'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} }

no olvide agregar también la importación para copiar:

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

Salida:

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

Si bien este código puede responder a la pregunta, proporcionar un contexto adicional con respecto a por qué y / o cómo responde a la pregunta mejora su valor a largo plazo.
xiawi

Creo que esta es una implementación genérica de fusionar uno o más diccionarios anidados teniendo en cuenta el tipo de los objetos que se van a marcar
Dorcioman,

0

echa un vistazo al toolzpaquete

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

da

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

0

La siguiente función combina b en 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

Y solo otra ligera variación:

Aquí hay una función de actualización profunda basada en un conjunto de python3 puro. Actualiza los diccionarios anidados recorriendo un nivel a la vez y se llama a sí mismo para actualizar cada siguiente nivel de valores del diccionario:

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

Un simple ejemplo:

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

¿Qué tal otra respuesta? Este también evita la mutación / efectos secundarios:

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)
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.