302 votes

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

Je cherche un moyen de mettre à jour le dictionnaire 1 avec le contenu du dictionnaire 2 sans écraser le niveau A.

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 la clé la plus basse du niveau 1.

Comment puis-je résoudre ce problème, étant donné que le dictionnaire 1 et la mise à jour peuvent avoir n'importe quelle longueur ?

0 votes

L'imbrication est-elle toujours de trois niveaux de profondeur ou peut-on avoir une imbrication d'une profondeur arbitraire ?

1 votes

Il peut avoir n'importe quelle profondeur/longueur.

0 votes

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.

403voto

Alex Martelli Points 330805

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 bug. 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 bogue apparaît quand la "mise à jour" a un k , v article où v est un dict et k n'est pas à l'origine une clé dans le dictionnaire qui est mis à jour -- le code de @FM "saute" cette partie de la mise à jour (parce qu'il l'exécute sur un new dict qui n'est ni sauvegardé ni retourné, mais perdu lorsque l'appel récursif revient).

Mes autres modifications sont mineures : il n'y a pas de raison pour que l'option if / else construire lorsque .get fait le même travail plus rapidement et plus proprement, et isinstance s'applique de préférence aux classes de base abstraites (et non aux classes concrètes) pour des raisons de généralité.

12 votes

+1 Bien vu pour le bug -- doh ! Je me suis dit que quelqu'un aurait une meilleure façon de gérer le problème du isinstance mais je me suis dit que j'allais tenter le coup.

0 votes

@jay_t. de rien -- oui, je suis d'accord que les classes de base abstraites du module collections (Mapping etc), qui étaient nouvelles dans Python 2.6, sont vraiment bien (vous pouvez aussi faire vos propres ABC avec le module abc!-).

0 votes

Pour moi, cela ne fonctionne que lorsque j'utilise dict au lieu de collections.Mapping ? ! Sinon, isinstance renvoie toujours false. (Python 2.5.4)

37voto

onosendi Points 92

J'ai mis un peu de temps sur celui-ci, mais grâce au post de @Alex, il a comblé le vide qui me manquait. Cependant, j'ai rencontré un problème si une valeur dans la récursive dict se trouve être un list J'ai donc pensé que je devais 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

4 votes

Je pense que cela devrait probablement être (pour être un peu plus sûr) : orig_dict.get(key, []) + val .

3 votes

Puisque les dicts sont mutables, vous changez l'instance que vous passez comme argument. Alors, vous n'avez pas besoin de retourner orig_dict.

4 votes

Je pense que la plupart des gens s'attendraient à ce que la définition renvoie la dictée mise à jour, même si elle est mise à jour en place.

35voto

charlax Points 3653

Même solution que celle acceptée, mais avec un nom de variable plus clair, une docstring, et la correction d'un bug où {} en tant que valeur ne serait pas remplacée.

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 charlatan paquet, en charlatan.utils .

22voto

bscan Points 1472

La réponse d'@Alex est bonne, mais ne fonctionne pas lorsqu'on remplace un élément tel qu'un entier par un dictionnaire, par exemple update({'foo':0},{'foo':{'bar':1}}) . Cette mise à jour y remédie :

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

17voto

kabirbaidhya Points 1164

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

Basé sur le rapport de @Alex Martelli. réponse .

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

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X