Aggiorna il valore di un dizionario nidificato di profondità variabile


163

Sto cercando un modo per aggiornare il dizionario dict1 con i contenuti dell'aggiornamento dict senza il livello di sovrascrittura A.

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

So che l'aggiornamento cancella i valori in level2 perché sta aggiornando la chiave più bassa level1.

Come potrei affrontarlo, dato che dizionario1 e l'aggiornamento possono avere qualsiasi lunghezza?


La nidificazione è sempre profonda tre livelli o puoi avere una nidificazione di profondità arbitraria?
ChristopheD,

Può avere qualsiasi profondità / lunghezza.
jay_t,

Correggimi se sbaglio, ma sembra che la soluzione ideale qui richieda l'implementazione del modello di progettazione composita.
Alexander McNulty,

Risposte:


264

La risposta di @ FM ha la giusta idea generale, cioè una soluzione ricorsiva, ma una codifica piuttosto peculiare e almeno un bug. Raccomanderei invece:

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

Il bug si presenta quando l '"aggiornamento" ha un k, velemento in cui vè un dicte knon è originariamente una chiave nel dizionario in fase di aggiornamento - il codice di @ FM "salta" questa parte dell'aggiornamento (perché lo esegue su un nuovo vuoto dictche non viene salvato o restituito da nessuna parte, semplicemente perso quando ritorna la chiamata ricorsiva).

Le mie altre modifiche sono minori: non vi è alcun motivo per il if/ elsecostrutto quando .getfa lo stesso lavoro più velocemente e in modo più pulito, ed isinstanceè meglio applicato a classi di base astratte (non concrete) per generalità.


7
+1 Buona cattura del bug - doh! Ho pensato che qualcuno avrebbe avuto un modo migliore per gestire il isinstancetest, ma ho pensato di fare un tentativo.
FMc,

6
Un'altra "caratteristica" minore fa aumentare questo TypeError: 'int' object does not support item assignment.quando, ad es update({'k1': 1}, {'k1': {'k2': 2}}). Per modificare questo comportamento e invece espandere la profondità dei dizionari per fare spazio a dizionari più profondi, è possibile aggiungere un elif isinstance(d, Mapping):intorno alla d[k] = u[k]e dopo la isinstancecondizione. Dovrai anche aggiungere un else: d = {k: u[k]}per affrontare il caso in cui il dict di aggiornamento è più profondo del dict originale. Felice di modificare la risposta, ma non voglio sporcare il codice conciso che risolve il problema del PO.
Piani cottura

1
Perché usare isinstance(v, collections.Mapping)piuttosto che isinstance(v, dict)? Nel caso in cui OP decida di iniziare a utilizzare le raccolte?
Matt

2
@Matt Yea, o qualsiasi altro oggetto derivato dalla mappatura (elenchi di coppie di cose). Rende la funzione più generale e meno probabile che ignori silenziosamente gli oggetti derivati ​​dalla mappatura e li lasci non aggiornati (errore insidioso che l'OP potrebbe non vedere / catturare mai). Devi quasi sempre usare Mapping per trovare tipi di dict e basestring per trovare tipi di str.
Piani cottura

2
Se stai eseguendo questo in Python 3+, cambia u.iteritems()in u.items(), altrimenti incontrerai:AttributeError: 'dict' object has no attribute 'iteritems'
Greg K il

23

Mi ha preso un po 'su questo, ma grazie al post di @ Alex, ha colmato il vuoto che mi mancava. Tuttavia, mi sono imbattuto in un problema se un valore all'interno del ricorsivo dictsembra essere un list, quindi ho pensato di condividere ed estendere la sua risposta.

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

3
Penso che questo dovrebbe essere probabilmente (per essere un po 'più sicuro): orig_dict.get(key, []) + val.
Andy Hayden,

2
Poiché i dicts sono mutabili, stai cambiando l'istanza che stai passando come argomento. Quindi, non è necessario restituire orig_dict.
gabrielhpugliese,

3
Penso che la maggior parte delle persone si aspetterebbe che la definizione restituisca il dict aggiornato anche se è stato aggiornato.
Kel Solaar,

La logica predefinita nel codice di onosendi è di aggiungere l'elenco aggiornato all'elenco originale. Se è necessario aggiornare sovrascrivere l'elenco originale, è necessario impostare orig_dict [chiave] = val
intijk

1
@gabrielhpugliese è necessario restituire l'originale se chiamato con un dizionario letterale, ad es. merged_tree = update({'default': {'initialvalue': 1}}, other_tree)
EoghanM,

18

@La risposta di Alex è buona, ma non funziona quando si sostituisce un elemento come un numero intero con un dizionario, ad esempio update({'foo':0},{'foo':{'bar':1}}). Questo aggiornamento lo risolve:

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

Vedo. Hai reso il mio elifcontrollo del tipo di oggetto originale una condizione "racchiusa" contenente i controlli sia del valore che della chiave di quel dict / mapping. Intelligente.
Piani cottura

Questo non funzionerà se il dict interno ha più di una chiave.
Wlerin,

@Wlerin, funziona ancora; D sarà diventato un Mapping da quel punto. Ecco un caso di test con più chiavi: update({'A1': 1, 'A2':2}, {'A1': {'B1': {'C1': 3, 'C2':4}, 'B2':2}, 'A3':5}). Hai un esempio che non fa quello che vuoi?
bscan,

Perché testare if isinstance(d, collections.Mapping)su ogni iterazione? Vedere la mia risposta .
Jérôme,

13

Stessa soluzione di quella accettata, ma denominazione delle variabili più chiara, docstring e risolto un bug in cui {}un valore non avrebbe avuto la precedenza.

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

Ecco alcuni casi di test:

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

Questa funzione è disponibile nel pacchetto ciarlatano , in charlatan.utils.


7

Ecco una versione immutabile della fusione di dizionario ricorsivo nel caso in cui qualcuno ne abbia bisogno.

Basato sulla risposta di @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

6

Piccoli miglioramenti alla risposta di @ Alex che consente l'aggiornamento di dizionari di diversa profondità e la limitazione della profondità con cui l'aggiornamento si tuffa nel dizionario nidificato originale (ma la profondità del dizionario di aggiornamento non è limitata). Sono stati testati solo pochi casi:

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

1
Grazie per questo! A quale caso d'uso potrebbe applicare il parametro di profondità?
Matt

@Matt quando hai alcuni oggetti / dicts a una profondità nota che non vuoi unire / aggiornare, solo sovrascritti con nuovi oggetti (come sostituire un dict con una stringa o un float o qualsiasi altra cosa, nel profondo del tuo dict)
Piani cottura

1
Funziona solo se l'aggiornamento è al massimo 1 livello più profondo dell'originale. Ad esempio, questo non riesce: update({'k1': 1}, {'k1': {'k2': {'k3': 3}}})ho aggiunto una risposta che risolve questo problema
bscan,

@bscan buona cattura! mai pensato a quel caso d'uso. Immagino che dovrei ricorrere più in profondità nei rami dell'elfo. Qualche idea?
Piani cottura

Perché testare if isinstance(d, Mapping)su ogni iterazione? Vedere la mia risposta . (Inoltre, non sono sicuro del tuo d = {k: u[k]})
Jérôme,

4

Questa domanda è vecchia, ma sono atterrato qui durante la ricerca di una soluzione di "fusione profonda". Le risposte sopra hanno ispirato ciò che segue. Ho finito per scrivere il mio perché c'erano bug in tutte le versioni che ho testato. Il punto critico mancato è stato, a una profondità arbitraria dei due dicts di input, per qualche chiave, k, l'albero decisionale quando d [k] o u [k] non è un dict era difettoso.

Inoltre, questa soluzione non richiede ricorsione, che è più simmetrica rispetto a come dict.update()funziona e restituisce 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))

4

Usa python-benedict (l'ho fatto) , ha un mergemetodo di utilità (aggiornato) e molti altri. Funziona con Python 2 / Python 3 ed è ben testato.

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

Installazione: pip install python-benedict

Documentazione: https://github.com/fabiocaccamo/python-benedict


2

In nessuna di queste risposte gli autori sembrano comprendere il concetto di aggiornamento di un oggetto memorizzato in un dizionario e neppure di iterare su voci del dizionario (al contrario delle chiavi). Quindi ho dovuto scriverne uno che non rende inutili negozi e reperti di dizionari tautologici. Si presume che i dadi vengano memorizzati altri dadi o tipi semplici.

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 ancora più semplice lavorare con qualsiasi 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

2

Aggiorna alla risposta di @Alex Martelli per correggere un bug nel suo codice per rendere la soluzione più solida:

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 chiave è che spesso vogliamo creare lo stesso tipo durante la ricorsione, quindi qui usiamo v.copy().clear()ma non {}. E questo è particolarmente utile se il dictqui è di tipo collections.defaultdictche può avere diversi tipi di default_factorys.

Si noti inoltre che u.iteritems()è stato modificato u.items()in Python3.


2

Ho usato la soluzione suggerita da @Alex Martelli, ma non ci riesce

TypeError 'bool' object does not support item assignment

quando i due dizionari differiscono nel tipo di dati a un certo livello.

Nel caso in cui allo stesso livello l'elemento del dizionario dsia solo uno scalare (cioè. Bool) Mentre l'elemento del dizionario uè ancora dizionario, la riassegnazione non riesce poiché non è possibile assegnare il dizionario allo scalare (come True[k]).

Una condizione aggiunta risolve che:

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

2

Il codice seguente dovrebbe risolvere il update({'k1': 1}, {'k1': {'k2': 2}})problema nella risposta di @Alex Martelli nel modo giusto.

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

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

usare dictocollections.Mapping


1

So che questa domanda è piuttosto vecchia, ma pubblica ancora quello che faccio quando devo aggiornare un dizionario nidificato. Possiamo usare il fatto che i dadi vengono passati per riferimento in Python Supponendo che il percorso della chiave sia noto e sia separato da punti. Forex se abbiamo un dict chiamato data:

{
"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/"
}

E vogliamo aggiornare la classe della coda, il percorso della chiave sarebbe - log_config_worker.handlers.queue.class

Possiamo usare la seguente funzione per aggiornare il valore:

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

Ciò aggiornerebbe correttamente il dizionario.


1

Potrebbe essere che ti imbatti in un dizionario non standard, come me oggi, che non ha attributi iteritems. In questo caso è facile interpretare questo tipo di dizionario come un dizionario standard. Ad esempio: 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()

0

Sì! E un'altra soluzione. La mia soluzione differisce nelle chiavi che vengono verificate. In tutte le altre soluzioni osserviamo solo le chiavi dict_b. Ma qui guardiamo nell'unione di entrambi i dizionari.

Fallo come preferisci

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

0

Se vuoi sostituire un "dizionario nidificato completo con array" puoi usare questo frammento:

Sostituirà qualsiasi "vecchio_valore" con "nuovo_valore". Sta approssimativamente facendo una prima ricostruzione profonda del dizionario. Può persino funzionare con List o Str / int forniti come parametro di input di primo livello.

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

0

Un altro modo di usare la ricorsione:

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)

0

un nuovo Q come da una catena di chiavi

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

0

potresti provare questo, funziona con le liste ed è 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

0

Consiglio di sostituirlo {}con type(v)()al fine di propagare il tipo di oggetto di qualsiasi sottoclasse di dict archiviata uma assente da d. Ad esempio, ciò preserverebbe tipi come collezioni.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

-1

Questo è un po 'a parte ma hai davvero bisogno di dizionari nidificati? A seconda del problema, a volte un dizionario semplice può essere sufficiente ... e avere un bell'aspetto:

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

5
La struttura nidificata proviene da set di dati JSON in arrivo, quindi vorrei mantenerli intatti, ...
jay_t

-1

Se vuoi un one-liner:

{**dictionary1, **{'level1':{**dictionary1['level1'], **{'level2':{**dictionary1['level1']['level2'], **{'levelB':10}}}}}}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.