Mettre à jour la valeur d'un dictionnaire imbriqué de profondeur variable

163

Je cherche un moyen de mettre à jour dict dictionary1 avec le contenu de la mise à jour de dict sans le niveau d'écrasementA

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

Je sais que la mise à jour supprime les valeurs du niveau 2 car elle met à jour le niveau de clé le plus bas1.

Comment pourrais-je m'y attaquer, étant donné que le dictionnaire1 et la mise à jour peuvent avoir n'importe quelle longueur?

jay_t
la source
La nidification a-t-elle toujours trois niveaux de profondeur ou pouvez-vous avoir une imbrication d'une profondeur arbitraire?
ChristopheD
Il peut avoir n'importe quelle profondeur / longueur.
jay_t
Corrigez-moi si je me trompe, mais il semble que la solution idéale ici nécessite la mise en œuvre du modèle de conception composite.
Alexander McNulty
un nouveau Q stackoverflow.com/questions/59004746/…
user7337353

Réponses:

264

La réponse de @ FM a la bonne idée générale, c'est-à-dire une solution récursive, mais un codage un peu particulier et au moins un bogue. Je recommanderais plutôt:

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

Le bug apparaît lorsque la « mise à jour » a un k, vpoint où vest dictet kn'est pas à l' origine une clé dans le dictionnaire étant mis à jour - @ code « Les skips » FM cette partie de la mise à jour (parce qu'il ne se produit sur un vide nouvelle dictqui n'est pas enregistré ni retourné nulle part, juste perdu lorsque l'appel récursif revient).

Mes autres changements sont mineurs: il n'y a aucune raison pour que la construction if/ elsequand .geteffectue le même travail plus rapidement et plus proprement, et isinstanceest mieux appliquée aux classes de base abstraites (pas concrètes) pour la généralité.

Alex Martelli
la source
7
+1 Bonne prise sur le bug - doh! Je pensais que quelqu'un aurait une meilleure façon de gérer le isinstancetest, mais j'ai pensé que je tenterais d'y répondre.
FMc
6
Une autre "fonctionnalité" mineure provoque cette augmentation TypeError: 'int' object does not support item assignment.lorsque vous, par exemple update({'k1': 1}, {'k1': {'k2': 2}}). Pour modifier ce comportement et augmenter la profondeur des dictionnaires pour faire de la place pour des dictionnaires plus approfondis, vous pouvez ajouter un elif isinstance(d, Mapping):autour d[k] = u[k]de la isinstancecondition et après . Vous devrez également ajouter un else: d = {k: u[k]}pour traiter le cas où le dict de mise à jour est plus profond que le dict d'origine. Heureux de modifier la réponse, mais ne voulez pas salir de code concis qui résout le problème de l'OP.
plaques de cuisson
1
Pourquoi utiliser isinstance(v, collections.Mapping)plutôt que isinstance(v, dict)? Dans le cas où OP déciderait de commencer à utiliser des collections?
Matt
2
@Matt Yea, ou tout autre objet dérivé de la cartographie (listes de paires de choses). Rend la fonction plus générale et moins susceptible d'ignorer discrètement les objets dérivés de mappage et de les laisser non mis à jour (erreur insidieuse que l'OP pourrait ne jamais voir / attraper). Vous souhaitez presque toujours utiliser Mapping pour rechercher des types de dict et une chaîne de base pour rechercher des types de chaînes.
plaques de cuisson
2
Si vous exécutez cela sous Python 3+, changez u.iteritems()en u.items(), sinon vous rencontrerez:AttributeError: 'dict' object has no attribute 'iteritems'
Greg K
23

Cela m'a pris un peu de temps sur celui-ci, mais grâce à la publication de @ Alex, il a comblé le vide qui me manquait. Cependant, je suis tombé sur un problème si une valeur dans le récursif dictse trouve être a list, alors j'ai pensé partager et étendre sa réponse.

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
la source
3
Je pense que cela devrait probablement être (être un peu plus sûr): orig_dict.get(key, []) + val.
Andy Hayden
2
Puisque les dicts sont mutables, vous modifiez l'instance que vous passez en argument. Ensuite, vous n'avez pas besoin de renvoyer orig_dict.
gabrielhpugliese
3
Je pense que la plupart des gens s'attendraient à ce que la définition renvoie le dict mis à jour même s'il est mis à jour en place.
Kel Solaar
La logique par défaut dans le code onosendi consiste à ajouter la liste mise à jour à la liste d'origine. Si vous avez besoin de mettre à jour pour écraser la liste d'origine, vous devez définir orig_dict [key] = val
intijk
1
@gabrielhpugliese renvoyant l'original est nécessaire s'il est appelé avec un littéral de dictionnaire, par exemple merged_tree = update({'default': {'initialvalue': 1}}, other_tree)
EoghanM
18

La réponse de @ Alex est bonne, mais ne fonctionne pas lors du remplacement d'un élément tel qu'un entier par un dictionnaire, tel que update({'foo':0},{'foo':{'bar':1}}). Cette mise à jour résout le problème:

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
la source
Je vois. Vous avez fait de ma elifvérification du type d'objet d'origine une condition "englobante" contenant les vérifications à la fois de la valeur et de la clé de ce dict / mappage. Intelligent.
plaques de cuisson
Cela ne fonctionnera pas si le dict interne a plus d'une clé.
Wlerin
@Wlerin, ça marche toujours; d sera devenu une cartographie à ce stade. Voici un test avec plusieurs clés: update({'A1': 1, 'A2':2}, {'A1': {'B1': {'C1': 3, 'C2':4}, 'B2':2}, 'A3':5}). Avez-vous un exemple qui ne fait pas ce que vous voulez?
bscan
Pourquoi tester if isinstance(d, collections.Mapping)sur chaque itération? Voyez ma réponse .
Jérôme le
13

Même solution que celle acceptée, mais nommage des variables plus claires, docstring et correction d'un bogue où {}une valeur ne remplaçait pas.

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

Voici quelques cas de 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}}

Cette fonction est disponible dans le package charlatan , au formatcharlatan.utils .

charlax
la source
7

Voici une version immuable de la fusion de dictionnaires récursive au cas où quelqu'un en aurait besoin.

Basé sur la réponse 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
la source
6

Améliorations mineures de la réponse de @ Alex qui permet de mettre à jour des dictionnaires de différentes profondeurs ainsi que de limiter la profondeur à laquelle la mise à jour plonge dans le dictionnaire imbriqué d'origine (mais la profondeur de mise à jour du dictionnaire n'est pas limitée). Seuls quelques cas ont été testés:

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
plaques de cuisson
la source
1
Merci pour cela! À quel cas d'utilisation le paramètre de profondeur peut-il s'appliquer?
Matt
@Matt lorsque vous avez des objets / dictées à une profondeur connue que vous ne voulez pas fusionner / mettre à jour, juste écrasés par de nouveaux objets (comme remplacer un dict par une chaîne ou un flottant ou autre, au fond de votre dict)
hobs
1
Cela ne fonctionne que si la mise à jour est au plus 1 niveau plus profonde que l'original. Par exemple, cela échoue: update({'k1': 1}, {'k1': {'k2': {'k3': 3}}})j'ai ajouté une réponse qui résout ce
problème
@bscan bonne prise! jamais pensé à ce cas d'utilisation. Je suppose que je devrais rentrer plus profondément dans les branches elif. Des idées?
plaques de cuisson
Pourquoi tester if isinstance(d, Mapping)sur chaque itération? Voyez ma réponse . (Aussi, je ne suis pas sûr de votre d = {k: u[k]})
Jérôme
4

Cette question est ancienne, mais j'ai atterri ici en cherchant une solution de "fusion profonde". Les réponses ci-dessus ont inspiré ce qui suit. J'ai fini par écrire le mien car il y avait des bugs dans toutes les versions que j'ai testées. Le point critique manqué était, à une certaine profondeur arbitraire des deux dicts d'entrée, pour une clé, k, l'arbre de décision lorsque d [k] ou u [k] n'est pas un dict était défectueux.

En outre, cette solution ne nécessite pas de récursivité, qui est plus symétrique avec la façon dont dict.update()fonctionne et retourne 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
la source
4

Utilisez simplement python-benedict (je l'ai fait) , il a une mergeméthode utilitaire (deepupdate) et bien d'autres. Cela fonctionne avec python 2 / python 3 et il est bien testé.

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

Installation: pip install python-benedict

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

Fabio Caccamo
la source
2

Dans aucune de ces réponses, les auteurs ne semblent comprendre le concept de mise à jour d'un objet stocké dans un dictionnaire ni même d'itération sur les éléments du dictionnaire (par opposition aux clés). J'ai donc dû en écrire un qui ne fasse pas de stockage et d'extraction de dictionnaires tautologiques inutiles. Les dictionnaires sont supposés stocker d'autres dictionnaires ou types 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

Ou encore plus simple fonctionnant avec n'importe quel type:

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
la source
2

Mettez à jour la réponse de @Alex Martelli pour corriger un bogue dans son code afin de rendre la solution plus robuste:

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 clé est que nous voulons souvent créer le même type à la récursivité, donc ici nous utilisons v.copy().clear()mais pas {}. Et ceci est particulièrement utile si l' dictici est de type collections.defaultdictqui peut avoir différents types de default_factorys.

Notez également que le u.iteritems()a été changé u.items()en Python3.

thuzhf
la source
2

J'ai utilisé la solution suggérée par @Alex Martelli, mais elle échoue

TypeError 'bool' object does not support item assignment

lorsque les deux dictionnaires diffèrent par le type de données à un certain niveau.

Dans le cas où au même niveau l'élément de dictionnaire dest juste un scalaire (c'est-à-dire. Bool) Tandis que l'élément de dictionnaire uest toujours dictionnaire, la réaffectation échoue car aucune affectation de dictionnaire n'est possible en scalaire (comme True[k]).

Une condition supplémentaire 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
la source
2

Le code ci-dessous devrait résoudre le update({'k1': 1}, {'k1': {'k2': 2}})problème de la réponse de @Alex Martelli de la bonne manière.

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
la source
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

utiliser dictoucollections.Mapping

honmaple
la source
1

Je sais que cette question est assez ancienne, mais je poste toujours ce que je fais lorsque je dois mettre à jour un dictionnaire imbriqué. On peut utiliser le fait que les dicts sont passés par référence en python En supposant que le chemin de la clé est connu et séparé par des points. Forex si nous avons un dict nommé 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/"
}

Et nous voulons mettre à jour la classe de file d'attente, le chemin de la clé serait - log_config_worker.handlers.queue.class

Nous pouvons utiliser la fonction suivante pour mettre à jour la valeur:

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

Cela mettrait à jour correctement le dictionnaire.

ipsuri
la source
1

Il se peut que vous tombiez sur un dictionnaire non standard, comme moi aujourd'hui, qui n'a pas d'itéritems-Attribute. Dans ce cas, il est facile d'interpréter ce type de dictionnaire comme un dictionnaire standard. Par exemple: 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
la source
0

Oui! Et une autre solution. Ma solution diffère dans les clés qui sont vérifiées. Dans toutes les autres solutions, nous ne regardons que les clés dict_b. Mais ici, nous regardons dans l'union des deux dictionnaires.

Faites-en ce que vous voulez

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
la source
0

Si vous souhaitez remplacer un "dictionnaire imbriqué complet par des tableaux", vous pouvez utiliser cet extrait de code:

Il remplacera toute "ancienne_valeur" par "nouvelle_valeur". Il s'agit en gros d'une reconstruction du dictionnaire en profondeur. Il peut même fonctionner avec List ou Str / int donné comme paramètre d'entrée du premier niveau.

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
la source
0

Une autre façon d'utiliser la récursivité:

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
la source
0

un nouveau Q comment Par un porte-clés

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}}
user7337353
la source
0

vous pouvez essayer ceci, cela fonctionne avec des listes et est pur:

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.
la source
0

Je recommande de remplacer {}par type(v)()afin de propager le type d'objet de toute sous-classe de dict stockée umais absente de d. Par exemple, cela préserverait les types tels que les collections.

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
la source
-1

C'est un peu à l'écart mais avez-vous vraiment besoin de dictionnaires imbriqués? Selon le problème, un dictionnaire plat peut parfois suffire ... et le regarder 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
la source
5
La structure imbriquée provient des ensembles de données json entrants, je voudrais donc les garder intacts, ...
jay_t
-1

Si vous voulez un monoplace:

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