Actualizar el valor de un diccionario anidado de profundidad variable

162

Estoy buscando una manera de actualizar dict dictionary1 con el contenido de la actualización dict sin sobrescribir el nivel A

dictionary1={'level1':{'level2':{'levelA':0,'levelB':1}}}
update={'level1':{'level2':{'levelB':10}}}
dictionary1.update(update)
print dictionary1
{'level1': {'level2': {'levelB': 10}}}

Sé que la actualización elimina los valores en el nivel 2 porque está actualizando la clave más baja nivel1.

¿Cómo podría abordar esto, dado que dictionary1 y update pueden tener cualquier extensión?

jay_t
fuente
¿La anidación es siempre de tres niveles de profundidad o puede tener una anidación de una profundidad arbitraria?
ChristopheD
Puede tener cualquier profundidad / longitud.
jay_t
Corríjame si me equivoco, pero parece que la solución ideal aquí requiere la implementación del patrón de diseño compuesto.
Alexander McNulty

Respuestas:

263

La respuesta de @ FM tiene la idea general correcta, es decir, una solución recursiva, pero una codificación algo peculiar y al menos un error. Yo recomendaría, en cambio:

Python 2:

import collections

def update(d, u):
    for k, v in u.iteritems():
        if isinstance(v, collections.Mapping):
            d[k] = update(d.get(k, {}), v)
        else:
            d[k] = v
    return d

Python 3:

import collections.abc

def update(d, u):
    for k, v in u.items():
        if isinstance(v, collections.abc.Mapping):
            d[k] = update(d.get(k, {}), v)
        else:
            d[k] = v
    return d

El error aparece cuando la "actualización" tiene un elemento k, vdonde ves un dicty kno es originalmente una clave en el diccionario que se está actualizando - el código de @ FM "omite" esta parte de la actualización (porque lo realiza en un nuevo vacío dictque no se guarda ni se devuelve en ningún lugar, solo se pierde cuando vuelve la llamada recursiva).

Mis otros cambios son menores: no hay ninguna razón para el if/ elseconstruct cuando .gethace el mismo trabajo más rápido y limpio, y isinstancese aplica mejor a las clases base abstractas (no a las concretas) por generalidad.

Alex Martelli
fuente
77
+1 Buena captura del error - ¡Doh! Pensé que alguien tendría una mejor manera de manejar la isinstanceprueba, pero pensé en intentarlo.
FMc
66
Otra "característica" menor hace que esto aumente TypeError: 'int' object does not support item assignment.cuando usted, por ejemplo update({'k1': 1}, {'k1': {'k2': 2}}). Para cambiar este comportamiento y, en su lugar, expandir la profundidad de los diccionarios para dar cabida a diccionarios más profundos, puede agregar un elif isinstance(d, Mapping):alrededor de d[k] = u[k]la isinstancecondición y después de ella . También necesitará agregar un else: d = {k: u[k]}para lidiar con el caso de que el dict de actualización es más profundo que el dict original. Feliz de editar la respuesta, pero no quiero ensuciar el código conciso que resuelve el problema del OP.
placas
1
¿Por qué usar en isinstance(v, collections.Mapping)lugar de isinstance(v, dict)? En el caso de que OP decida comenzar a usar colecciones?
Matt
2
@Matt Yea, o cualquier otro objeto derivado de mapeo (listas de pares de cosas). Hace que la función sea más general y menos probable que ignore silenciosamente los objetos derivados de la asignación y los deje sin actualizar (error insidioso que el OP podría no ver / atrapar). Casi siempre desea usar Mapping para buscar tipos de dict y cadenas de base para encontrar tipos str.
placas
2
Si está ejecutando esto en Python 3+ cambie u.iteritems()a u.items(), de lo contrario encontrará:AttributeError: 'dict' object has no attribute 'iteritems'
Greg K
23

Me tomó un poco en este caso, pero gracias a la publicación de @ Alex, completó el vacío que me faltaba. Sin embargo, me encontré con un problema si un valor dentro del recursivo dictresulta ser un list, así que pensé en compartirlo y ampliar su respuesta.

import collections

def update(orig_dict, new_dict):
    for key, val in new_dict.iteritems():
        if isinstance(val, collections.Mapping):
            tmp = update(orig_dict.get(key, { }), val)
            orig_dict[key] = tmp
        elif isinstance(val, list):
            orig_dict[key] = (orig_dict.get(key, []) + val)
        else:
            orig_dict[key] = new_dict[key]
    return orig_dict
Nate Glenn
fuente
3
Creo que esto probablemente debería ser (a ser un poco más seguro): orig_dict.get(key, []) + val.
Andy Hayden
2
Como los dictados son mutables, está cambiando la instancia que está pasando como argumento. Entonces, no necesita devolver orig_dict.
gabrielhpugliese
3
Creo que la mayoría de la gente esperaría que la definición devuelva el dict actualizado aunque esté actualizado en su lugar.
Kel Solaar
La lógica predeterminada en el código de onosendi es agregar la lista actualizada a la lista original. Si necesita actualizar, sobrescribir la lista original, debe configurar orig_dict [key] = val
intijk el
1
@gabrielhpugliese devolver el original es necesario si se llama con un diccionario literal, por ejemplo merged_tree = update({'default': {'initialvalue': 1}}, other_tree)
EoghanM
18

@ La respuesta de Alex es buena, pero no funciona cuando se reemplaza un elemento como un entero con un diccionario, como update({'foo':0},{'foo':{'bar':1}}). Esta actualización lo aborda:

import collections
def update(d, u):
    for k, v in u.iteritems():
        if isinstance(d, collections.Mapping):
            if isinstance(v, collections.Mapping):
                r = update(d.get(k, {}), v)
                d[k] = r
            else:
                d[k] = u[k]
        else:
            d = {k: u[k]}
    return d

update({'k1': 1}, {'k1': {'k2': {'k3': 3}}})
bscan
fuente
Veo. Hizo que mi elifverificación del tipo de objeto original fuera un condicional "envolvente" que contiene las comprobaciones tanto del valor como de la clave de ese dict / mapping. Inteligente.
encimeras
Esto no funcionará si el dict interno tiene más de una clave.
Wlerin
@Wlerin, todavía funciona; d se habrá convertido en un mapeo en ese punto. He aquí un caso de prueba con varias claves: update({'A1': 1, 'A2':2}, {'A1': {'B1': {'C1': 3, 'C2':4}, 'B2':2}, 'A3':5}). ¿Tienes un ejemplo que no hace lo que quieres?
bscan
¿Por qué probar if isinstance(d, collections.Mapping)en cada iteración? Mira mi respuesta .
Jérôme
13

La misma solución que la aceptada, pero un nombre variable más claro, una cadena de documentos y un error corregido donde, {}como valor, no se anularía.

import collections


def deep_update(source, overrides):
    """
    Update a nested dictionary or similar mapping.
    Modify ``source`` in place.
    """
    for key, value in overrides.iteritems():
        if isinstance(value, collections.Mapping) and value:
            returned = deep_update(source.get(key, {}), value)
            source[key] = returned
        else:
            source[key] = overrides[key]
    return source

Aquí hay algunos casos de prueba:

def test_deep_update():
    source = {'hello1': 1}
    overrides = {'hello2': 2}
    deep_update(source, overrides)
    assert source == {'hello1': 1, 'hello2': 2}

    source = {'hello': 'to_override'}
    overrides = {'hello': 'over'}
    deep_update(source, overrides)
    assert source == {'hello': 'over'}

    source = {'hello': {'value': 'to_override', 'no_change': 1}}
    overrides = {'hello': {'value': 'over'}}
    deep_update(source, overrides)
    assert source == {'hello': {'value': 'over', 'no_change': 1}}

    source = {'hello': {'value': 'to_override', 'no_change': 1}}
    overrides = {'hello': {'value': {}}}
    deep_update(source, overrides)
    assert source == {'hello': {'value': {}, 'no_change': 1}}

    source = {'hello': {'value': {}, 'no_change': 1}}
    overrides = {'hello': {'value': 2}}
    deep_update(source, overrides)
    assert source == {'hello': {'value': 2, 'no_change': 1}}

Esta función está disponible en el paquete charlatán , en charlatan.utils.

charlax
fuente
7

Aquí hay una versión inmutable de la fusión recursiva de diccionarios en caso de que alguien la necesite.

Basado en la respuesta de @Alex Martelli .

Python 2.x:

import collections
from copy import deepcopy


def merge(dict1, dict2):
    ''' Return a new dictionary by merging two dictionaries recursively. '''

    result = deepcopy(dict1)

    for key, value in dict2.iteritems():
        if isinstance(value, collections.Mapping):
            result[key] = merge(result.get(key, {}), value)
        else:
            result[key] = deepcopy(dict2[key])

    return result

Python 3.x:

import collections
from copy import deepcopy


def merge(dict1, dict2):
    ''' Return a new dictionary by merging two dictionaries recursively. '''

    result = deepcopy(dict1)

    for key, value in dict2.items():
        if isinstance(value, collections.Mapping):
            result[key] = merge(result.get(key, {}), value)
        else:
            result[key] = deepcopy(dict2[key])

    return result
kabirbaidhya
fuente
6

Pequeñas mejoras en la respuesta de @ Alex que permite actualizar diccionarios de diferentes profundidades, así como limitar la profundidad en que la actualización se sumerge en el diccionario anidado original (pero la profundidad de actualización del diccionario no está limitada). Solo unos pocos casos han sido probados:

def update(d, u, depth=-1):
    """
    Recursively merge or update dict-like objects. 
    >>> update({'k1': {'k2': 2}}, {'k1': {'k2': {'k3': 3}}, 'k4': 4})
    {'k1': {'k2': {'k3': 3}}, 'k4': 4}
    """

    for k, v in u.iteritems():
        if isinstance(v, Mapping) and not depth == 0:
            r = update(d.get(k, {}), v, depth=max(depth - 1, -1))
            d[k] = r
        elif isinstance(d, Mapping):
            d[k] = u[k]
        else:
            d = {k: u[k]}
    return d
placas
fuente
1
¡Gracias por esto! ¿A qué caso de uso podría aplicarse el parámetro de profundidad?
Matt
@Matt cuando tiene algunos objetos / dictados a una profundidad conocida que no desea fusionar / actualizar, simplemente sobrescritos con nuevos objetos (como reemplazar un dict con una cadena o flotante o lo que sea, en lo profundo de su dict)
hobs
1
Esto solo funciona si la actualización es como máximo 1 nivel más profunda que la original. Por ejemplo, esto falla: update({'k1': 1}, {'k1': {'k2': {'k3': 3}}})agregué una respuesta que aborda esto
bscan
@bscan buena captura! Nunca pensé en ese caso de uso. Supongo que debería recurrir más profundamente en las ramas elif. ¿Algunas ideas?
encimeras
¿Por qué probar if isinstance(d, Mapping)en cada iteración? Mira mi respuesta . (Además, no estoy seguro de tu d = {k: u[k]})
Jérôme
4

Esta pregunta es antigua, pero aterricé aquí cuando buscaba una solución de "fusión profunda". Las respuestas anteriores inspiraron lo que sigue. Terminé escribiendo el mío porque había errores en todas las versiones que probé. El punto crítico perdido fue, a cierta profundidad arbitraria de los dos dictados de entrada, para alguna clave, k, el árbol de decisión cuando d [k] o u [k] no es un dict fue defectuoso.

Además, esta solución no requiere recursividad, que es más simétrica con el dict.update()funcionamiento y los retornos None.

import collections
def deep_merge(d, u):
   """Do a deep merge of one dict into another.

   This will update d with values in u, but will not delete keys in d
   not found in u at some arbitrary depth of d. That is, u is deeply
   merged into d.

   Args -
     d, u: dicts

   Note: this is destructive to d, but not u.

   Returns: None
   """
   stack = [(d,u)]
   while stack:
      d,u = stack.pop(0)
      for k,v in u.items():
         if not isinstance(v, collections.Mapping):
            # u[k] is not a dict, nothing to merge, so just set it,
            # regardless if d[k] *was* a dict
            d[k] = v
         else:
            # note: u[k] is a dict

            # get d[k], defaulting to a dict, if it doesn't previously
            # exist
            dv = d.setdefault(k, {})

            if not isinstance(dv, collections.Mapping):
               # d[k] is not a dict, so just set it to u[k],
               # overriding whatever it was
               d[k] = v
            else:
               # both d[k] and u[k] are dicts, push them on the stack
               # to merge
               stack.append((dv, v))
djpinne
fuente
4

Simplemente use python-benedict (lo hice) , tiene un mergemétodo de utilidad (actualización profunda) y muchos otros. Funciona con python 2 / python 3 y está bien probado.

from benedict import benedict

dictionary1=benedict({'level1':{'level2':{'levelA':0,'levelB':1}}})
update={'level1':{'level2':{'levelB':10}}}
dictionary1.merge(update)
print(dictionary1)
# >> {'level1':{'level2':{'levelA':0,'levelB':10}}}

Instalación: pip install python-benedict

Documentación: https://github.com/fabiocaccamo/python-benedict

Fabio Caccamo
fuente
2

En ninguna de estas respuestas, los autores parecen entender el concepto de actualizar un objeto almacenado en un diccionario, ni siquiera de iterar sobre elementos del diccionario (a diferencia de las claves). Así que tuve que escribir uno que no haga inútiles almacenes y recuperaciones de diccionarios tautológicos. Se supone que los dictos almacenan otros dictados o tipos simples.

def update_nested_dict(d, other):
    for k, v in other.items():
        if isinstance(v, collections.Mapping):
            d_v = d.get(k)
            if isinstance(d_v, collections.Mapping):
                update_nested_dict(d_v, v)
            else:
                d[k] = v.copy()
        else:
            d[k] = v

O incluso más simple trabajando con cualquier tipo:

def update_nested_dict(d, other):
    for k, v in other.items():
        d_v = d.get(k)
        if isinstance(v, collections.Mapping) and isinstance(d_v, collections.Mapping):
            update_nested_dict(d_v, v)
        else:
            d[k] = deepcopy(v) # or d[k] = v if you know what you're doing
panda-34
fuente
2

Actualice la respuesta de @Alex Martelli para corregir un error en su código para hacer que la solución sea más sólida:

def update_dict(d, u):
    for k, v in u.items():
        if isinstance(v, collections.Mapping):
            default = v.copy()
            default.clear()
            r = update_dict(d.get(k, default), v)
            d[k] = r
        else:
            d[k] = v
    return d

La clave es que a menudo queremos crear el mismo tipo en la recursividad, por lo que aquí usamos v.copy().clear()pero no {}. Y esto es especialmente útil si el dicthere es de tipo collections.defaultdictque puede tener diferentes tipos de default_factorys.

Observe también que u.iteritems()se ha cambiado a u.items()in Python3.

Thuzhf
fuente
2

Usé la solución que sugiere @Alex Martelli, pero falla

TypeError 'bool' object does not support item assignment

cuando los dos diccionarios difieren en tipo de datos en algún nivel.

En el caso de que al mismo nivel, el elemento del diccionario dsea ​​solo un escalar (es decir Bool), mientras que el elemento del diccionario usigue siendo el diccionario, la reasignación falla ya que no es posible la asignación del diccionario al escalar (como True[k]).

Una condición adicional corrige que:

from collections import Mapping

def update_deep(d, u):
    for k, v in u.items():
        # this condition handles the problem
        if not isinstance(d, Mapping):
            d = u
        elif isinstance(v, Mapping):
            r = update_deep(d.get(k, {}), v)
            d[k] = r
        else:
            d[k] = u[k]

    return d
helvete
fuente
2

El siguiente código debería resolver el update({'k1': 1}, {'k1': {'k2': 2}})problema en la respuesta de @Alex Martelli de la manera correcta.

def deepupdate(original, update):
    """Recursively update a dict.

    Subdict's won't be overwritten but also updated.
    """
    if not isinstance(original, abc.Mapping):
        return update
    for key, value in update.items():
        if isinstance(value, abc.Mapping):
            original[key] = deepupdate(original.get(key, {}), value)
        else:
            original[key] = value
    return original
Jérôme
fuente
1
def update(value, nvalue):
    if not isinstance(value, dict) or not isinstance(nvalue, dict):
        return nvalue
    for k, v in nvalue.items():
        value.setdefault(k, dict())
        if isinstance(v, dict):
            v = update(value[k], v)
        value[k] = v
    return value

usar dictocollections.Mapping

honmaple
fuente
1

Sé que esta pregunta es bastante antigua, pero aún publico lo que hago cuando tengo que actualizar un diccionario anidado. Podemos usar el hecho de que los dictados se pasan por referencia en python Suponiendo que la ruta de la clave es conocida y está separada por puntos. Forex si tenemos un dict llamado datos:

{
"log_config_worker": {
    "version": 1, 
    "root": {
        "handlers": [
            "queue"
        ], 
        "level": "DEBUG"
    }, 
    "disable_existing_loggers": true, 
    "handlers": {
        "queue": {
            "queue": null, 
            "class": "myclass1.QueueHandler"
        }
    }
}, 
"number_of_archived_logs": 15, 
"log_max_size": "300M", 
"cron_job_dir": "/etc/cron.hourly/", 
"logs_dir": "/var/log/patternex/", 
"log_rotate_dir": "/etc/logrotate.d/"
}

Y queremos actualizar la clase de cola, la ruta de la clave sería: log_config_worker.handlers.queue.class

Podemos usar la siguiente función para actualizar el valor:

def get_updated_dict(obj, path, value):
    key_list = path.split(".")

    for k in key_list[:-1]:
        obj = obj[k]

    obj[key_list[-1]] = value

get_updated_dict(data, "log_config_worker.handlers.queue.class", "myclass2.QueueHandler")

Esto actualizaría el diccionario correctamente.

ipsuri
fuente
1

Podría ser que tropieces con un diccionario no estándar, como yo hoy, que no tiene atributos de iteritems. En este caso, es fácil interpretar este tipo de diccionario como un diccionario estándar. Por ejemplo: Python 2.7:

    import collections
    def update(orig_dict, new_dict):
        for key, val in dict(new_dict).iteritems():
            if isinstance(val, collections.Mapping):
                tmp = update(orig_dict.get(key, { }), val)
                orig_dict[key] = tmp
            elif isinstance(val, list):
                orig_dict[key] = (orig_dict[key] + val)
            else:
                orig_dict[key] = new_dict[key]
        return orig_dict

    import multiprocessing
    d=multiprocessing.Manager().dict({'sample':'data'})
    u={'other': 1234}

    x=update(d, u)
    x.items()

Python 3.8:

    def update(orig_dict, new_dict):
        orig_dict=dict(orig_dict)
        for key, val in dict(new_dict).items():
            if isinstance(val, collections.abc.Mapping):
                tmp = update(orig_dict.get(key, { }), val)
                orig_dict[key] = tmp
            elif isinstance(val, list):
                orig_dict[key] = (orig_dict[key] + val)
            else:
                orig_dict[key] = new_dict[key]
        return orig_dict

    import collections
    import multiprocessing
    d=multiprocessing.Manager().dict({'sample':'data'})
    u={'other': 1234, "deeper": {'very': 'deep'}}

    x=update(d, u)
    x.items()
noragen
fuente
0

¡Si! Y otra solución. Mi solución difiere en las teclas que se están comprobando. En todas las demás soluciones solo miramos las claves endict_b . Pero aquí miramos en la unión de ambos diccionarios.

Hazlo como quieras

def update_nested(dict_a, dict_b):
    set_keys = set(dict_a.keys()).union(set(dict_b.keys()))
    for k in set_keys:
        v = dict_a.get(k)
        if isinstance(v, dict):
            new_dict = dict_b.get(k, None)
            if new_dict:
                update_nested(v, new_dict)
        else:
            new_value = dict_b.get(k, None)
            if new_value:
                dict_a[k] = new_value
zwep
fuente
0

Si desea reemplazar un "diccionario anidado completo con matrices", puede usar este fragmento:

Reemplazará cualquier "viejo_valor" por "nuevo_valor". Se trata más o menos de una reconstrucción profunda del diccionario. Incluso puede funcionar con List o Str / int dado como parámetro de entrada de primer nivel.

def update_values_dict(original_dict, future_dict, old_value, new_value):
    # Recursively updates values of a nested dict by performing recursive calls

    if isinstance(original_dict, Dict):
        # It's a dict
        tmp_dict = {}
        for key, value in original_dict.items():
            tmp_dict[key] = update_values_dict(value, future_dict, old_value, new_value)
        return tmp_dict
    elif isinstance(original_dict, List):
        # It's a List
        tmp_list = []
        for i in original_dict:
            tmp_list.append(update_values_dict(i, future_dict, old_value, new_value))
        return tmp_list
    else:
        # It's not a dict, maybe a int, a string, etc.
        return original_dict if original_dict != old_value else new_value
ZettaCircl
fuente
0

Otra forma de usar la recursividad:

def updateDict(dict1,dict2):
    keys1 = list(dict1.keys())
    keys2= list(dict2.keys())
    keys2 = [x for x in keys2 if x in keys1]
    for x in keys2:
        if (x in keys1) & (type(dict1[x]) is dict) & (type(dict2[x]) is dict):
            updateDict(dict1[x],dict2[x])
        else:
            dict1.update({x:dict2[x]})
    return(dict1)
yifyan
fuente
0

un nuevo Q cómo hacerlo por una cadena de llaves

dictionary1={'level1':{'level2':{'levelA':0,'levelB':1}},'anotherLevel1':{'anotherLevel2':{'anotherLevelA':0,'anotherLevelB':1}}}
update={'anotherLevel1':{'anotherLevel2':1014}}
dictionary1.update(update)
print dictionary1
{'level1':{'level2':{'levelA':0,'levelB':1}},'anotherLevel1':{'anotherLevel2':1014}}
usuario7337353
fuente
0

podrías probar esto, funciona con listas y es puro:

def update_keys(newd, dic, mapping):
  def upsingle(d,k,v):
    if k in mapping:
      d[mapping[k]] = v
    else:
      d[k] = v
  for ekey, evalue in dic.items():
    upsingle(newd, ekey, evalue)
    if type(evalue) is dict:
      update_keys(newd, evalue, mapping)
    if type(evalue) is list:
      upsingle(newd, ekey, [update_keys({}, i, mapping) for i in evalue])
  return newd
Craig N.
fuente
0

Recomiendo reemplazar {}por type(v)()para propagar el tipo de objeto de cualquier subclase dict almacenada upero ausente d. Por ejemplo, esto preservaría tipos como colecciones. OrderedDict:

Python 2:

import collections

def update(d, u):
    for k, v in u.iteritems():
        if isinstance(v, collections.Mapping):
            d[k] = update(d.get(k, type(v)()), v)
        else:
            d[k] = v
    return d

Python 3:

import collections.abc

def update(d, u):
    for k, v in u.items():
        if isinstance(v, collections.abc.Mapping):
            d[k] = update(d.get(k, type(v)()), v)
        else:
            d[k] = v
    return d
Nico
fuente
-1

Eso es un poco al margen, pero ¿realmente necesita diccionarios anidados? Dependiendo del problema, a veces un diccionario plano puede ser suficiente ... y lucir bien:

>>> dict1 = {('level1','level2','levelA'): 0}
>>> dict1['level1','level2','levelB'] = 1
>>> update = {('level1','level2','levelB'): 10}
>>> dict1.update(update)
>>> print dict1
{('level1', 'level2', 'levelB'): 10, ('level1', 'level2', 'levelA'): 0}
Nas Banov
fuente
55
La estructura anidada proviene de entrantes conjuntos de datos JSON, por lo que me gustaría mantenerlas intactas, ...
jay_t
-1

Si quieres una sola línea:

{**dictionary1, **{'level1':{**dictionary1['level1'], **{'level2':{**dictionary1['level1']['level2'], **{'levelB':10}}}}}}
Joe '
fuente