Actualizar el valor de un diccionario nested de profundidad variable

Estoy buscando una forma de actualizar el diccionario de dict1 con el contenido de la actualización de 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 el nivel de clave más bajo1.

¿Cómo podría abordar esto, dado que el diccionario 1 y la actualización pueden tener alguna longitud?

La respuesta de @FM tiene la idea general correcta, es decir, una solución recursiva, pero una encoding 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 def update(d, u): for k, v in u.items(): if isinstance(v, collections.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 , v donde v es un dict k no es originalmente una clave en el diccionario que se está actualizando. El código de @FF “omite” esta parte de la actualización (porque la ejecuta) en un nuevo dict vacío que no se guarda ni se devuelve a ninguna parte, solo se pierde cuando vuelve la llamada recursiva).

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

Tomé un poco de esto, pero gracias a la publicación de @ Alex, él llenó el espacio que faltaba. Sin embargo, me encontré con un problema si un valor dentro del dict recursivo resulta ser una list , así que pensé que lo compartiría y extendería 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 

La respuesta de @ Alex es buena, pero no funciona cuando se reemplaza un elemento como un número entero por 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}}}) 

La misma solución que la aceptada, pero con nombres de variables, cadenas de documentos y errores más claros, donde se corrigió un error 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}} 

Estas funciones están disponibles en el paquete charlatan , en charlatan.utils .

Mejoras menores en la respuesta de @ Alex que permiten la actualización de diccionarios de diferentes profundidades, así como la limitación de la profundidad de la actualización en el diccionario nested original (pero la profundidad de actualización del diccionario no está limitada). Sólo 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 

Aquí hay una versión inmutable de la combinación recursiva del diccionario en caso de que alguien lo 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 

En ninguna de estas respuestas, los autores parecen entender el concepto de actualizar un objeto almacenado en un diccionario, ni siquiera de iterar sobre los elementos del diccionario (a diferencia de las claves). Así que tuve que escribir uno que no haga inútiles tiendas de diccionario tautológico y recuperaciones. Se asume que los dictados 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 sencillo 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 

Actualice la respuesta de @Alex Martelli para corregir un error en su código y 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 recursión, por lo que aquí usamos v.copy().clear() pero no {} . Y esto es especialmente útil si el dict aquí es de tipo collections.defaultdict que puede tener diferentes tipos de default_factory s.

También u.iteritems() cuenta que u.iteritems() se ha cambiado a u.items() en Python3 .

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

TypeError 'bool' object does not support item assignment

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

En el caso de que en el mismo nivel, el elemento del diccionario d sea ​​solo un escalar (es decir, Bool ), mientras que el elemento del diccionario u aún sea diccionario, la reasignación falla, ya que no es posible realizar una asignación de diccionario en 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 

Esta pregunta es antigua, pero llegué 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 omitido fue, a una profundidad arbitraria de los dos dictados de entrada, para alguna tecla, 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 recursión, que es más simétrica con el funcionamiento de dict.update() y no devuelve 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)) 

Puede ser que te tropieces con un diccionario no estándar, como yo hoy, que no tiene atributos iteritems. En este caso, es fácil interpretar este tipo de diccionario como un diccionario estándar. P.ej:

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

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

 { "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 acceso 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(dict_to_update, path, value): obj = dict_to_update 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.

 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 

utilizar dict o collections.Mapping

Eso es un poco hacia un lado, pero ¿realmente necesitas diccionarios nesteds? Dependiendo del problema, a veces el diccionario plano puede ser suficiente … y se ve bien en él:

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

Si quieres una sola línea:

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