Comment fusionner des dictionnaires de dictionnaires?


129

J'ai besoin de fusionner plusieurs dictionnaires, voici ce que j'ai par exemple:

dict1 = {1:{"a":{A}}, 2:{"b":{B}}}

dict2 = {2:{"c":{C}}, 3:{"d":{D}}

Avec A B C et Détant les feuilles de l'arbre, comme{"info1":"value", "info2":"value2"}

Il y a un niveau inconnu (profondeur) de dictionnaires, cela pourrait être {2:{"c":{"z":{"y":{C}}}}}

Dans mon cas, il représente une structure de répertoires / fichiers avec les nœuds étant des documents et les feuilles étant des fichiers.

Je souhaite les fusionner pour obtenir:

 dict3 = {1:{"a":{A}}, 2:{"b":{B},"c":{C}}, 3:{"d":{D}}}

Je ne sais pas comment je pourrais le faire facilement avec Python.


Que voulez-vous pour votre profondeur arbitraire de dictionnaires? Voulez-vous yaplatir au cniveau ou quoi? Votre exemple est incomplet.
agf

Vérifiez ma classe NestedDict ici: stackoverflow.com/a/16296144/2334951 Il gère les structures de dictionnaire imbriquées comme la fusion et plus encore.
SzieberthAdam

3
Un avertissement à tous ceux qui recherchent des solutions: cette question concerne uniquement les dictionnaires imbriqués. La plupart des réponses ne traitent pas correctement le cas plus compliqué des listes de dictées dans la structure. Si vous en avez besoin, essayez la réponse de @Osiloke ci-dessous: stackoverflow.com/a/25270947/1431660
SHernandez


Réponses:


143

c'est en fait assez délicat - en particulier si vous voulez un message d'erreur utile lorsque les choses sont incohérentes, tout en acceptant correctement les entrées dupliquées mais cohérentes (ce qu'aucune autre réponse ne fait ici ....)

en supposant que vous n'ayez pas un grand nombre d'entrées, une fonction récursive est la plus simple:

def merge(a, b, path=None):
    "merges b into a"
    if path is None: path = []
    for key in b:
        if key in a:
            if isinstance(a[key], dict) and isinstance(b[key], dict):
                merge(a[key], b[key], path + [str(key)])
            elif a[key] == b[key]:
                pass # same leaf value
            else:
                raise Exception('Conflict at %s' % '.'.join(path + [str(key)]))
        else:
            a[key] = b[key]
    return a

# works
print(merge({1:{"a":"A"},2:{"b":"B"}}, {2:{"c":"C"},3:{"d":"D"}}))
# has conflict
merge({1:{"a":"A"},2:{"b":"B"}}, {1:{"a":"A"},2:{"b":"C"}})

notez que cela mute a- le contenu de best ajouté à a(qui est également retourné). si vous voulez garder, avous pouvez l'appeler comme ça merge(dict(a), b).

agf a souligné (ci-dessous) que vous pouvez avoir plus de deux dictionnaires, auquel cas vous pouvez utiliser:

reduce(merge, [dict1, dict2, dict3...])

où tout sera ajouté à dict1.

[note - j'ai modifié ma réponse initiale pour faire muter le premier argument; qui rend le "réduire" plus facile à expliquer]

ps en python 3, vous aurez également besoin from functools import reduce


1
Vous pouvez ensuite le coller dans une reduceboucle ou une boucle équivalente pour travailler avec un nombre arbitraire de dicts au lieu de deux. Cependant, je ne suis pas sûr que cela fasse ce qu'il veut non plus (il n'était pas clair), vous vous retrouvez avec 2: {'c': {'z': {'y': {'info1': 'value', 'info2': 'value2'}}}, 'b': {'info1': 'value', 'info2': 'value2'}}son deuxième exemple, je ne sais pas s'il veut zet yaplati ou pas?
agf

1
ce sont des structures de répertoires donc je ne pense pas qu'il / elle veut quelque chose d'aplati? oh, désolé, j'ai manqué "plusieurs dictionnaires". oui, réduire serait bien. ajoutera cela.
andrew cooke

Cela fait exactement ce que je voulais! Je suis désolé de ne pas être assez clair ... Je pensais que j'étais d'accord avec Python, semble pas: - / J'avais besoin d'une fonction récursive à cause des dictionnaires imbriqués, celle-ci fonctionne et je peux le comprendre :) Je ne le fais pas semble être en mesure de le faire fonctionner avec réduire cependant ...
fdhex

2
Pour ceux qui ont des listes que le niveau imbriqué finale sous les dicts, vous pouvez le faire au lieu de soulever l'erreur de concaténer les deux listes: a[key] = a[key] + b[key]. Merci pour la réponse utile.
kevinmicke

1
> si vous voulez garder a, vous pouvez l'appeler comme merge (dict (a), b) Notez que les dictionnaires imbriqués seront toujours mutés. Pour éviter cela, utilisez copy.deepcopy.
rcorre

31

Voici un moyen simple de le faire à l'aide de générateurs:

def mergedicts(dict1, dict2):
    for k in set(dict1.keys()).union(dict2.keys()):
        if k in dict1 and k in dict2:
            if isinstance(dict1[k], dict) and isinstance(dict2[k], dict):
                yield (k, dict(mergedicts(dict1[k], dict2[k])))
            else:
                # If one of the values is not a dict, you can't continue merging it.
                # Value from second dict overrides one in first and we move on.
                yield (k, dict2[k])
                # Alternatively, replace this with exception raiser to alert you of value conflicts
        elif k in dict1:
            yield (k, dict1[k])
        else:
            yield (k, dict2[k])

dict1 = {1:{"a":"A"},2:{"b":"B"}}
dict2 = {2:{"c":"C"},3:{"d":"D"}}

print dict(mergedicts(dict1,dict2))

Cela imprime:

{1: {'a': 'A'}, 2: {'c': 'C', 'b': 'B'}, 3: {'d': 'D'}}

si vous voulez conserver le thème du générateur, vous pouvez enchaîner (dict1.keys (), dict2.keys ())
andrew cooke

Cela n'obtiendrait-il pas des clés en double?
jterrace

Celui-ci semble faire le travail, au moins sur mon ensemble de données, mais comme je n'ai jamais bien compris le rendement et les générateurs, je ne sais pas trop pourquoi, mais je vais essayer un peu plus fort, cela pourrait être utile!
fdhex

ah, oui, il obtiendrait des clés en double. vous auriez encore besoin de l'envelopper dans un ensemble, désolé.
andrew cooke

2
J'ai trouvé cela particulièrement utile. Mais le plus gentil serait de laisser la fonction résoudre les conflits en tant que paramètre.
mentatkgs

25

Un problème avec cette question est que les valeurs du dict peuvent être des éléments de données arbitrairement complexes. Sur la base de ces réponses et d'autres, j'ai trouvé ce code:

class YamlReaderError(Exception):
    pass

def data_merge(a, b):
    """merges b into a and return merged result

    NOTE: tuples and arbitrary objects are not handled as it is totally ambiguous what should happen"""
    key = None
    # ## debug output
    # sys.stderr.write("DEBUG: %s to %s\n" %(b,a))
    try:
        if a is None or isinstance(a, str) or isinstance(a, unicode) or isinstance(a, int) or isinstance(a, long) or isinstance(a, float):
            # border case for first run or if a is a primitive
            a = b
        elif isinstance(a, list):
            # lists can be only appended
            if isinstance(b, list):
                # merge lists
                a.extend(b)
            else:
                # append to list
                a.append(b)
        elif isinstance(a, dict):
            # dicts must be merged
            if isinstance(b, dict):
                for key in b:
                    if key in a:
                        a[key] = data_merge(a[key], b[key])
                    else:
                        a[key] = b[key]
            else:
                raise YamlReaderError('Cannot merge non-dict "%s" into dict "%s"' % (b, a))
        else:
            raise YamlReaderError('NOT IMPLEMENTED "%s" into "%s"' % (b, a))
    except TypeError, e:
        raise YamlReaderError('TypeError "%s" in key "%s" when merging "%s" into "%s"' % (e, key, b, a))
    return a

Mon cas d'utilisation est la fusion de fichiers YAML où je n'ai à gérer qu'un sous-ensemble de types de données possibles. Par conséquent, je peux ignorer les tuples et autres objets. Pour moi, une logique de fusion sensée signifie

  • remplacer les scalaires
  • ajouter des listes
  • fusionner les dicts en ajoutant des clés manquantes et en mettant à jour les clés existantes

Tout le reste et les imprévus entraînent une erreur.


1
Fantastique. Fonctionne également bien sur les décharges json. Je viens de supprimer la gestion des erreurs. (Être paresseux, peut faire les bons pour json, j'en suis sûr)
dgBP

3
la séquence "isinstance" peut être remplacée w / isinstance(a, (str, unicode, int, long, float))isnt 'it?
simahawk

12

Fusion de dictionnaires de dictionnaires

Comme il s'agit de la question canonique (en dépit de certaines non-généralités), je propose l'approche pythonique canonique pour résoudre ce problème.

Cas le plus simple: "les feuilles sont des dictionnaires imbriqués qui se terminent par des dictionnaires vides":

d1 = {'a': {1: {'foo': {}}, 2: {}}}
d2 = {'a': {1: {}, 2: {'bar': {}}}}
d3 = {'b': {3: {'baz': {}}}}
d4 = {'a': {1: {'quux': {}}}}

C'est le cas le plus simple pour la récursivité, et je recommanderais deux approches naïves:

def rec_merge1(d1, d2):
    '''return new merged dict of dicts'''
    for k, v in d1.items(): # in Python 2, use .iteritems()!
        if k in d2:
            d2[k] = rec_merge1(v, d2[k])
    d3 = d1.copy()
    d3.update(d2)
    return d3

def rec_merge2(d1, d2):
    '''update first dict with second recursively'''
    for k, v in d1.items(): # in Python 2, use .iteritems()!
        if k in d2:
            d2[k] = rec_merge2(v, d2[k])
    d1.update(d2)
    return d1

Je crois que je préférerais le second au premier, mais gardez à l'esprit que l'état d'origine du premier devrait être reconstruit à partir de son origine. Voici l'utilisation:

>>> from functools import reduce # only required for Python 3.
>>> reduce(rec_merge1, (d1, d2, d3, d4))
{'a': {1: {'quux': {}, 'foo': {}}, 2: {'bar': {}}}, 'b': {3: {'baz': {}}}}
>>> reduce(rec_merge2, (d1, d2, d3, d4))
{'a': {1: {'quux': {}, 'foo': {}}, 2: {'bar': {}}}, 'b': {3: {'baz': {}}}}

Cas complexe: "les feuilles sont de tout autre type:"

Donc, s'ils se terminent par des dictionnaires, c'est un simple cas de fusion de la fin des dictionnaires vides. Sinon, ce n'est pas si banal. Si des chaînes, comment les fusionner? Les ensembles peuvent être mis à jour de la même manière, nous pourrions donc appliquer ce traitement, mais nous perdons l'ordre dans lequel ils ont été fusionnés. L'ordre est-il donc important?

Donc, au lieu de plus d'informations, l'approche la plus simple sera de leur donner le traitement de mise à jour standard si les deux valeurs ne sont pas des dictées: c'est-à-dire que la valeur du second dict écrasera la première, même si la valeur du second dict est None et la valeur du premier est un dict avec beaucoup d'informations.

d1 = {'a': {1: 'foo', 2: None}}
d2 = {'a': {1: None, 2: 'bar'}}
d3 = {'b': {3: 'baz'}}
d4 = {'a': {1: 'quux'}}

from collections import MutableMapping

def rec_merge(d1, d2):
    '''
    Update two dicts of dicts recursively, 
    if either mapping has leaves that are non-dicts, 
    the second's leaf overwrites the first's.
    '''
    for k, v in d1.items(): # in Python 2, use .iteritems()!
        if k in d2:
            # this next check is the only difference!
            if all(isinstance(e, MutableMapping) for e in (v, d2[k])):
                d2[k] = rec_merge(v, d2[k])
            # we could further check types and merge as appropriate here.
    d3 = d1.copy()
    d3.update(d2)
    return d3

Et maintenant

from functools import reduce
reduce(rec_merge, (d1, d2, d3, d4))

Retour

{'a': {1: 'quux', 2: 'bar'}, 'b': {3: 'baz'}}

Application à la question initiale:

J'ai dû supprimer les accolades autour des lettres et les mettre entre guillemets simples pour que cela soit légitime Python (sinon, ils seraient définis comme des littéraux dans Python 2.7+) et ajouter une accolade manquante:

dict1 = {1:{"a":'A'}, 2:{"b":'B'}}
dict2 = {2:{"c":'C'}, 3:{"d":'D'}}

et rec_merge(dict1, dict2)retourne maintenant:

{1: {'a': 'A'}, 2: {'c': 'C', 'b': 'B'}, 3: {'d': 'D'}}

Qui correspond au résultat souhaité de la question initiale (après modification, par exemple le {A}à 'A'.)


10

Basé sur @andrew cooke. Cette version gère les listes imbriquées de dictionnaires et permet également de mettre à jour les valeurs

def merge(a, b, path=None, update=True):
    "http://stackoverflow.com/questions/7204805/python-dictionaries-of-dictionaries-merge"
    "merges b into a"
    if path is None: path = []
    for key in b:
        if key in a:
            if isinstance(a[key], dict) and isinstance(b[key], dict):
                merge(a[key], b[key], path + [str(key)])
            elif a[key] == b[key]:
                pass # same leaf value
            elif isinstance(a[key], list) and isinstance(b[key], list):
                for idx, val in enumerate(b[key]):
                    a[key][idx] = merge(a[key][idx], b[key][idx], path + [str(key), str(idx)], update=update)
            elif update:
                a[key] = b[key]
            else:
                raise Exception('Conflict at %s' % '.'.join(path + [str(key)]))
        else:
            a[key] = b[key]
    return a

1
Merci, c'est si utile. J'ai des listes de dicts dans mes structures tout le temps, les autres solutions ne peuvent pas fusionner correctement cela.
SHernandez

7

Cette procédure récursive simple fusionnera un dictionnaire dans un autre tout en remplaçant les clés en conflit:

#!/usr/bin/env python2.7

def merge_dicts(dict1, dict2):
    """ Recursively merges dict2 into dict1 """
    if not isinstance(dict1, dict) or not isinstance(dict2, dict):
        return dict2
    for k in dict2:
        if k in dict1:
            dict1[k] = merge_dicts(dict1[k], dict2[k])
        else:
            dict1[k] = dict2[k]
    return dict1

print (merge_dicts({1:{"a":"A"}, 2:{"b":"B"}}, {2:{"c":"C"}, 3:{"d":"D"}}))
print (merge_dicts({1:{"a":"A"}, 2:{"b":"B"}}, {1:{"a":"A"}, 2:{"b":"C"}}))

Production:

{1: {'a': 'A'}, 2: {'c': 'C', 'b': 'B'}, 3: {'d': 'D'}}
{1: {'a': 'A'}, 2: {'b': 'C'}}

7

Basé sur les réponses de @andrew cooke. Il prend mieux en charge les listes imbriquées.

def deep_merge_lists(original, incoming):
    """
    Deep merge two lists. Modifies original.
    Recursively call deep merge on each correlated element of list. 
    If item type in both elements are
     a. dict: Call deep_merge_dicts on both values.
     b. list: Recursively call deep_merge_lists on both values.
     c. any other type: Value is overridden.
     d. conflicting types: Value is overridden.

    If length of incoming list is more that of original then extra values are appended.
    """
    common_length = min(len(original), len(incoming))
    for idx in range(common_length):
        if isinstance(original[idx], dict) and isinstance(incoming[idx], dict):
            deep_merge_dicts(original[idx], incoming[idx])

        elif isinstance(original[idx], list) and isinstance(incoming[idx], list):
            deep_merge_lists(original[idx], incoming[idx])

        else:
            original[idx] = incoming[idx]

    for idx in range(common_length, len(incoming)):
        original.append(incoming[idx])


def deep_merge_dicts(original, incoming):
    """
    Deep merge two dictionaries. Modifies original.
    For key conflicts if both values are:
     a. dict: Recursively call deep_merge_dicts on both values.
     b. list: Call deep_merge_lists on both values.
     c. any other type: Value is overridden.
     d. conflicting types: Value is overridden.

    """
    for key in incoming:
        if key in original:
            if isinstance(original[key], dict) and isinstance(incoming[key], dict):
                deep_merge_dicts(original[key], incoming[key])

            elif isinstance(original[key], list) and isinstance(incoming[key], list):
                deep_merge_lists(original[key], incoming[key])

            else:
                original[key] = incoming[key]
        else:
            original[key] = incoming[key]

intuitif et symétrique. +1 pour la gestion de la liste :)
vdwees

6

Si vous avez un niveau inconnu de dictionnaires, alors je suggérerais une fonction récursive:

def combineDicts(dictionary1, dictionary2):
    output = {}
    for item, value in dictionary1.iteritems():
        if dictionary2.has_key(item):
            if isinstance(dictionary2[item], dict):
                output[item] = combineDicts(value, dictionary2.pop(item))
        else:
            output[item] = value
    for item, value in dictionary2.iteritems():
         output[item] = value
    return output

5

Aperçu

L'approche suivante subdivise le problème d'une fusion profonde des dictionnaires en:

  1. Une fonction de fusion superficielle paramétrée merge(f)(a,b)qui utilise une fonction fpour fusionner deux dictionnaires aetb

  2. Une fonction de fusion récursive fà utiliser avecmerge


la mise en oeuvre

Une fonction de fusion de deux dictionnaires (non imbriqués) peut être écrite de nombreuses manières. J'aime personnellement

def merge(f):
    def merge(a,b): 
        keys = a.keys() | b.keys()
        return {key:f(a.get(key), b.get(key)) for key in keys}
    return merge

Une bonne façon de définir une fonction de fusion récursive appropriée fconsiste à utiliser multipledispatch qui permet de définir des fonctions qui évaluent le long de différents chemins en fonction du type de leurs arguments.

from multipledispatch import dispatch

#for anything that is not a dict return
@dispatch(object, object)
def f(a, b):
    return b if b is not None else a

#for dicts recurse 
@dispatch(dict, dict)
def f(a,b):
    return merge(f)(a,b)

Exemple

Pour fusionner deux dictionnaires imbriqués, utilisez simplement merge(f)par exemple:

dict1 = {1:{"a":"A"},2:{"b":"B"}}
dict2 = {2:{"c":"C"},3:{"d":"D"}}
merge(f)(dict1, dict2)
#returns {1: {'a': 'A'}, 2: {'b': 'B', 'c': 'C'}, 3: {'d': 'D'}} 

Remarques:

Les avantages de cette approche sont:

  • La fonction est construite à partir de fonctions plus petites qui font chacune une seule chose, ce qui rend le code plus simple à raisonner et à tester

  • Le comportement n'est pas codé en dur mais peut être modifié et étendu selon les besoins, ce qui améliore la réutilisation du code (voir l'exemple ci-dessous).


Personnalisation

Certaines réponses ont également considéré des dictionnaires contenant des listes, par exemple d'autres dictionnaires (potentiellement imbriqués). Dans ce cas, on peut vouloir cartographier les listes et les fusionner en fonction de la position. Cela peut être fait en ajoutant une autre définition à la fonction de fusion f:

import itertools
@dispatch(list, list)
def f(a,b):
    return [merge(f)(*arg) for arg in itertools.zip_longest(a, b)]

4

Au cas où quelqu'un voudrait encore une autre approche à ce problème, voici ma solution.

Vertus : courtes, déclaratives et fonctionnelles dans le style (récursif, sans mutation).

Inconvénient potentiel : ce n'est peut-être pas la fusion que vous recherchez. Consultez la docstring pour la sémantique.

def deep_merge(a, b):
    """
    Merge two values, with `b` taking precedence over `a`.

    Semantics:
    - If either `a` or `b` is not a dictionary, `a` will be returned only if
      `b` is `None`. Otherwise `b` will be returned.
    - If both values are dictionaries, they are merged as follows:
        * Each key that is found only in `a` or only in `b` will be included in
          the output collection with its value intact.
        * For any key in common between `a` and `b`, the corresponding values
          will be merged with the same semantics.
    """
    if not isinstance(a, dict) or not isinstance(b, dict):
        return a if b is None else b
    else:
        # If we're here, both a and b must be dictionaries or subtypes thereof.

        # Compute set of all keys in both dictionaries.
        keys = set(a.keys()) | set(b.keys())

        # Build output dictionary, merging recursively values with common keys,
        # where `None` is used to mean the absence of a value.
        return {
            key: deep_merge(a.get(key), b.get(key))
            for key in keys
        }

Réponse très intéressante, merci de la partager. Quelle syntaxe avez-vous utilisée après l'instruction return? Je ne suis pas au courant.
dev_does_software

4

Vous pouvez essayer mergedeep .


Installation

$ pip3 install mergedeep

Usage

from mergedeep import merge

a = {"keyA": 1}
b = {"keyB": {"sub1": 10}}
c = {"keyB": {"sub2": 20}}

merge(a, b, c) 

print(a)
# {"keyA": 1, "keyB": {"sub1": 10, "sub2": 20}}

Pour une liste complète des options, consultez la documentation !


3

Il y a un léger problème avec la réponse d'Andrew Cookes: dans certains cas, cela modifie le deuxième argument blorsque vous modifiez le dict renvoyé. Plus précisément, c'est à cause de cette ligne:

if key in a:
    ...
else:
    a[key] = b[key]

Si b[key]est a dict, il sera simplement attribué à a, ce qui signifie que toute modification ultérieure dictaffectera à la fois aet b.

a={}
b={'1':{'2':'b'}}
c={'1':{'3':'c'}}
merge(merge(a,b), c) # {'1': {'3': 'c', '2': 'b'}}
a # {'1': {'3': 'c', '2': 'b'}} (as expected)
b # {'1': {'3': 'c', '2': 'b'}} <----
c # {'1': {'3': 'c'}} (unmodified)

Pour résoudre ce problème, la ligne devrait être remplacée par ceci:

if isinstance(b[key], dict):
    a[key] = clone_dict(b[key])
else:
    a[key] = b[key]

clone_dictest:

def clone_dict(obj):
    clone = {}
    for key, value in obj.iteritems():
        if isinstance(value, dict):
            clone[key] = clone_dict(value)
        else:
            clone[key] = value
    return

Encore. Cela ne tient évidemment pas compte list, setet d'autres choses, mais j'espère que cela illustre les pièges lors de la tentative de fusion dicts.

Et par souci d'exhaustivité, voici ma version, où vous pouvez la passer plusieurs dicts:

def merge_dicts(*args):
    def clone_dict(obj):
        clone = {}
        for key, value in obj.iteritems():
            if isinstance(value, dict):
                clone[key] = clone_dict(value)
            else:
                clone[key] = value
        return

    def merge(a, b, path=[]):
        for key in b:
            if key in a:
                if isinstance(a[key], dict) and isinstance(b[key], dict):
                    merge(a[key], b[key], path + [str(key)])
                elif a[key] == b[key]:
                    pass
                else:
                    raise Exception('Conflict at `{path}\''.format(path='.'.join(path + [str(key)])))
            else:
                if isinstance(b[key], dict):
                    a[key] = clone_dict(b[key])
                else:
                    a[key] = b[key]
        return a
    return reduce(merge, args, {})

Pourquoi pas deepcopyau lieu de clone_dict?
Armando Pérez Marqués

1
Parce que le python stdlib est vraiment énorme et magnifique! Je n'avais aucune idée que cela existait - en plus c'était une petite chose amusante à coder :-)
andsens

2

Cette version de la fonction prendra en compte N nombre de dictionnaires, et seulement les dictionnaires - aucun paramètre incorrect ne peut être passé, ou cela déclenchera une TypeError. La fusion elle-même tient compte des conflits de clés et, au lieu d'écraser les données d'un dictionnaire plus bas dans la chaîne de fusion, elle crée un ensemble de valeurs et les ajoute; aucune donnée n'est perdue.

Ce n'est peut-être pas le plus efficace sur la page, mais c'est le plus complet et vous ne perdrez aucune information lorsque vous fusionnerez vos 2 à N dictionnaires.

def merge_dicts(*dicts):
    if not reduce(lambda x, y: isinstance(y, dict) and x, dicts, True):
        raise TypeError, "Object in *dicts not of type dict"
    if len(dicts) < 2:
        raise ValueError, "Requires 2 or more dict objects"


    def merge(a, b):
        for d in set(a.keys()).union(b.keys()):
            if d in a and d in b:
                if type(a[d]) == type(b[d]):
                    if not isinstance(a[d], dict):
                        ret = list({a[d], b[d]})
                        if len(ret) == 1: ret = ret[0]
                        yield (d, sorted(ret))
                    else:
                        yield (d, dict(merge(a[d], b[d])))
                else:
                    raise TypeError, "Conflicting key:value type assignment"
            elif d in a:
                yield (d, a[d])
            elif d in b:
                yield (d, b[d])
            else:
                raise KeyError

    return reduce(lambda x, y: dict(merge(x, y)), dicts[1:], dicts[0])

print merge_dicts({1:1,2:{1:2}},{1:2,2:{3:1}},{4:4})

sortie: {1: [1, 2], 2: {1: 2, 3: 1}, 4: 4}


2

Étant donné que dictviews prend en charge les opérations d'ensemble, j'ai pu grandement simplifier la réponse de jterrace.

def merge(dict1, dict2):
    for k in dict1.keys() - dict2.keys():
        yield (k, dict1[k])

    for k in dict2.keys() - dict1.keys():
        yield (k, dict2[k])

    for k in dict1.keys() & dict2.keys():
        yield (k, dict(merge(dict1[k], dict2[k])))

Toute tentative de combiner un dict avec un non dict (techniquement, un objet avec une méthode 'keys' et un objet sans méthode 'keys') lèvera une AttributeError. Cela inclut à la fois l'appel initial à la fonction et les appels récursifs. C'est exactement ce que je voulais alors je l'ai laissé. Vous pouvez facilement attraper un AttributeErrors levé par l'appel récursif et ensuite donner n'importe quelle valeur que vous voulez.


2

Short-n-sweet:

from collections.abc import MutableMapping as Map

def nested_update(d, v):
"""
Nested update of dict-like 'd' with dict-like 'v'.
"""

for key in v:
    if key in d and isinstance(d[key], Map) and isinstance(v[key], Map):
        nested_update(d[key], v[key])
    else:
        d[key] = v[key]

Cela fonctionne comme (et est construit sur) la dict.updateméthode de Python . Il retourne None(vous pouvez toujours ajouter return dsi vous préférez) car il met à jour le dict dsur place. Les clés vécrasent toutes les clés existantes dansd (il n'essaye pas d'interpréter le contenu du dict).

Il fonctionnera également pour d'autres mappages ("de type dict").


1

Le code dépendra de vos règles de résolution des conflits de fusion, bien sûr. Voici une version qui peut prendre un nombre arbitraire d'arguments et les fusionne de manière récursive à une profondeur arbitraire, sans utiliser de mutation d'objet. Il utilise les règles suivantes pour résoudre les conflits de fusion:

  • les dictionnaires ont priorité sur les valeurs non dict ( {"foo": {...}}a priorité sur{"foo": "bar"} )
  • arguments ultérieurs ont priorité sur les arguments précédents (si vous fusionnez {"a": 1}, {"a", 2}et {"a": 3}dans l' ordre, le résultat sera {"a": 3})
try:
    from collections import Mapping
except ImportError:
    Mapping = dict

def merge_dicts(*dicts):                                                            
    """                                                                             
    Return a new dictionary that is the result of merging the arguments together.   
    In case of conflicts, later arguments take precedence over earlier arguments.   
    """                                                                             
    updated = {}                                                                    
    # grab all keys                                                                 
    keys = set()                                                                    
    for d in dicts:                                                                 
        keys = keys.union(set(d))                                                   

    for key in keys:                                                                
        values = [d[key] for d in dicts if key in d]                                
        # which ones are mapping types? (aka dict)                                  
        maps = [value for value in values if isinstance(value, Mapping)]            
        if maps:                                                                    
            # if we have any mapping types, call recursively to merge them          
            updated[key] = merge_dicts(*maps)                                       
        else:                                                                       
            # otherwise, just grab the last value we have, since later arguments    
            # take precedence over earlier arguments                                
            updated[key] = values[-1]                                               
    return updated  

1

J'avais deux dictionnaires ( aet b) qui pouvaient chacun contenir n'importe quel nombre de dictionnaires imbriqués. Je voulais les fusionner récursivement, en bprenant le pas sur a.

Considérant les dictionnaires imbriqués comme des arbres, ce que je voulais était:

  • Pour mettre à jour de asorte que chaque chemin vers chaque feuille de bsoit représenté dansa
  • Pour écraser les sous-arborescences asi une feuille est trouvée dans le chemin correspondant dansb
    • Conservez l'invariant selon lequel tous bles nœuds feuilles restent des feuilles.

Les réponses existantes étaient un peu compliquées à mon goût et laissaient quelques détails sur l'étagère. J'ai piraté ensemble ce qui suit, qui passe des tests unitaires pour mon ensemble de données.

  def merge_map(a, b):
    if not isinstance(a, dict) or not isinstance(b, dict):
      return b

    for key in b.keys():
      a[key] = merge_map(a[key], b[key]) if key in a else b[key]
    return a

Exemple (formaté pour plus de clarté):

 a = {
    1 : {'a': 'red', 
         'b': {'blue': 'fish', 'yellow': 'bear' },
         'c': { 'orange': 'dog'},
    },
    2 : {'d': 'green'},
    3: 'e'
  }

  b = {
    1 : {'b': 'white'},
    2 : {'d': 'black'},
    3: 'e'
  }


  >>> merge_map(a, b)
  {1: {'a': 'red', 
       'b': 'white',
       'c': {'orange': 'dog'},},
   2: {'d': 'black'},
   3: 'e'}

Les chemins bqui devaient être entretenus étaient:

  • 1 -> 'b' -> 'white'
  • 2 -> 'd' -> 'black'
  • 3 -> 'e'.

a avait les chemins uniques et non conflictuels de:

  • 1 -> 'a' -> 'red'
  • 1 -> 'c' -> 'orange' -> 'dog'

ils sont donc toujours représentés dans la carte fusionnée.


1

J'ai une solution itérative - fonctionne beaucoup mieux avec les gros dictionnaires et beaucoup d'entre eux (par exemple jsons, etc.):

import collections


def merge_dict_with_subdicts(dict1: dict, dict2: dict) -> dict:
    """
    similar behaviour to builtin dict.update - but knows how to handle nested dicts
    """
    q = collections.deque([(dict1, dict2)])
    while len(q) > 0:
        d1, d2 = q.pop()
        for k, v in d2.items():
            if k in d1 and isinstance(d1[k], dict) and isinstance(v, dict):
                q.append((d1[k], v))
            else:
                d1[k] = v

    return dict1

notez que cela utilisera la valeur de d2 pour remplacer d1, au cas où ils ne seraient pas tous les deux. (identique à pythondict.update() )

quelques tests:

def test_deep_update():
    d = dict()
    merge_dict_with_subdicts(d, {"a": 4})
    assert d == {"a": 4}

    new_dict = {
        "b": {
            "c": {
                "d": 6
            }
        }
    }
    merge_dict_with_subdicts(d, new_dict)
    assert d == {
        "a": 4,
        "b": {
            "c": {
                "d": 6
            }
        }
    }

    new_dict = {
        "a": 3,
        "b": {
            "f": 7
        }
    }
    merge_dict_with_subdicts(d, new_dict)
    assert d == {
        "a": 3,
        "b": {
            "c": {
                "d": 6
            },
            "f": 7
        }
    }

    # test a case where one of the dicts has dict as value and the other has something else
    new_dict = {
        'a': {
            'b': 4
        }
    }
    merge_dict_with_subdicts(d, new_dict)
    assert d['a']['b'] == 4

J'ai testé avec environ 1200 dictionnaires - cette méthode a pris 0,4 seconde, tandis que la solution récursive a pris environ 2,5 secondes.


0

Cela devrait aider à fusionner tous les éléments de dict2dans dict1:

for item in dict2:
    if item in dict1:
        for leaf in dict2[item]:
            dict1[item][leaf] = dict2[item][leaf]
    else:
        dict1[item] = dict2[item]

Veuillez le tester et nous dire si c'est ce que vous vouliez.

ÉDITER:

La solution mentionnée ci-dessus ne fusionne qu'un seul niveau, mais résout correctement l'exemple donné par OP. Pour fusionner plusieurs niveaux, la récursivité doit être utilisée.


1
Il a une profondeur de nidification arbitraire
agf

Cela peut être réécrit simplement comme for k,v in dict2.iteritems(): dict1.setdefault(k,{}).update(v). Mais comme @agf l'a souligné, cela ne fusionne pas les dictionnaires imbriqués.
Shawn Chin

@agf: Correct, il semble donc qu'OP ait besoin d'une solution utilisant la récurrence. Grâce au fait que les dictionnaires sont modifiables, cela devrait être assez facile à faire. Mais je pense que la question n'est pas assez précise pour dire ce qui devrait se passer lorsque nous arrivons à des endroits avec différents niveaux de profondeur (par exemple en essayant de fusionner {'a':'b'}avec {'a':{'c':'d'}).
Tadeck

0

J'ai testé vos solutions et j'ai décidé d'utiliser celle-ci dans mon projet:

def mergedicts(dict1, dict2, conflict, no_conflict):
    for k in set(dict1.keys()).union(dict2.keys()):
        if k in dict1 and k in dict2:
            yield (k, conflict(dict1[k], dict2[k]))
        elif k in dict1:
            yield (k, no_conflict(dict1[k]))
        else:
            yield (k, no_conflict(dict2[k]))

dict1 = {1:{"a":"A"}, 2:{"b":"B"}}
dict2 = {2:{"c":"C"}, 3:{"d":"D"}}

#this helper function allows for recursion and the use of reduce
def f2(x, y):
    return dict(mergedicts(x, y, f2, lambda x: x))

print dict(mergedicts(dict1, dict2, f2, lambda x: x))
print dict(reduce(f2, [dict1, dict2]))

Passer des fonctions en tant que paramètres est la clé pour étendre la solution jterrace afin qu'elle se comporte comme toutes les autres solutions récursives.


0

Le moyen le plus simple auquel je puisse penser est:

#!/usr/bin/python

from copy import deepcopy
def dict_merge(a, b):
    if not isinstance(b, dict):
        return b
    result = deepcopy(a)
    for k, v in b.iteritems():
        if k in result and isinstance(result[k], dict):
                result[k] = dict_merge(result[k], v)
        else:
            result[k] = deepcopy(v)
    return result

a = {1:{"a":'A'}, 2:{"b":'B'}}
b = {2:{"c":'C'}, 3:{"d":'D'}}

print dict_merge(a,b)

Production:

{1: {'a': 'A'}, 2: {'c': 'C', 'b': 'B'}, 3: {'d': 'D'}}

0

J'ai une autre solution légèrement différente ici:

def deepMerge(d1, d2, inconflict = lambda v1,v2 : v2) :
''' merge d2 into d1. using inconflict function to resolve the leaf conflicts '''
    for k in d2:
        if k in d1 : 
            if isinstance(d1[k], dict) and isinstance(d2[k], dict) :
                deepMerge(d1[k], d2[k], inconflict)
            elif d1[k] != d2[k] :
                d1[k] = inconflict(d1[k], d2[k])
        else :
            d1[k] = d2[k]
    return d1

Par défaut, il résout les conflits en faveur des valeurs du second dict, mais vous pouvez facilement le remplacer, avec un peu de sorcellerie, vous pourrez même en supprimer des exceptions. :).


0
class Utils(object):

    """

    >>> a = { 'first' : { 'all_rows' : { 'pass' : 'dog', 'number' : '1' } } }
    >>> b = { 'first' : { 'all_rows' : { 'fail' : 'cat', 'number' : '5' } } }
    >>> Utils.merge_dict(b, a) == { 'first' : { 'all_rows' : { 'pass' : 'dog', 'fail' : 'cat', 'number' : '5' } } }
    True

    >>> main = {'a': {'b': {'test': 'bug'}, 'c': 'C'}}
    >>> suply = {'a': {'b': 2, 'd': 'D', 'c': {'test': 'bug2'}}}
    >>> Utils.merge_dict(main, suply) == {'a': {'b': {'test': 'bug'}, 'c': 'C', 'd': 'D'}}
    True

    """

    @staticmethod
    def merge_dict(main, suply):
        """
        获取融合的字典,以main为主,suply补充,冲突时以main为准
        :return:
        """
        for key, value in suply.items():
            if key in main:
                if isinstance(main[key], dict):
                    if isinstance(value, dict):
                        Utils.merge_dict(main[key], value)
                    else:
                        pass
                else:
                    pass
            else:
                main[key] = value
        return main

if __name__ == '__main__':
    import doctest
    doctest.testmod()

0

hé là j'ai aussi eu le même problème mais je pensais à une solution et je la posterai ici, au cas où cela serait également utile pour d'autres, en gros en fusionnant des dictionnaires imbriqués et en ajoutant également les valeurs, pour moi j'avais besoin de calculer certaines probabilités donc cela on a très bien fonctionné:

#used to copy a nested dict to a nested dict
def deepupdate(target, src):
    for k, v in src.items():
        if k in target:
            for k2, v2 in src[k].items():
                if k2 in target[k]:
                    target[k][k2]+=v2
                else:
                    target[k][k2] = v2
        else:
            target[k] = copy.deepcopy(v)

en utilisant la méthode ci-dessus, nous pouvons fusionner:

target = {'6,6': {'6,63': 1}, '63, 4 ': {' 4,4 ': 1},' 4,4 ': {' 4,3 ': 1} , '6,63': {'63, 4 ': 1}}

src = {'5,4': {'4,4': 1}, '5,5': {'5,4': 1}, '4,4': {'4,3': 1} }

et cela deviendra: {'5,5': {'5,4': 1}, '5,4': {'4,4': 1}, '6,6': {'6,63' : 1}, '63, 4 ': {' 4,4 ': 1},' 4,4 ': {' 4,3 ': 2},' 6,63 ': {'63, 4': 1 }}

notez également les changements ici:

cible = {'6,6': {'6,63': 1}, '6,63': {'63, 4 ': 1}, ' 4,4 ': {' 4,3 ': 1} , '63, 4 ': {' 4,4 ': 1}}

src = {'5,4': {'4,4': 1}, '4,3': {'3,4': 1}, '4,4': {'4,9': 1} , '3,4': {'4,4': 1}, '5,5': {'5,4': 1}}

merge = {'5,4': {'4,4': 1}, '4,3': {'3,4': 1}, '6,63': {'63, 4 ': 1} , '5,5': {'5,4': 1}, '6,6': {'6,63': 1}, '3,4': {'4,4': 1}, ' 63,4 ': {' 4,4 ': 1}, ' 4,4 ': {' 4,3 ': 1,' 4,9 ': 1} }

n'oubliez pas d'ajouter également l'importation pour copie:

import copy

0
from collections import defaultdict
from itertools import chain

class DictHelper:

@staticmethod
def merge_dictionaries(*dictionaries, override=True):
    merged_dict = defaultdict(set)
    all_unique_keys = set(chain(*[list(dictionary.keys()) for dictionary in dictionaries]))  # Build a set using all dict keys
    for key in all_unique_keys:
        keys_value_type = list(set(filter(lambda obj_type: obj_type != type(None), [type(dictionary.get(key, None)) for dictionary in dictionaries])))
        # Establish the object type for each key, return None if key is not present in dict and remove None from final result
        if len(keys_value_type) != 1:
            raise Exception("Different objects type for same key: {keys_value_type}".format(keys_value_type=keys_value_type))

        if keys_value_type[0] == list:
            values = list(chain(*[dictionary.get(key, []) for dictionary in dictionaries]))  # Extract the value for each key
            merged_dict[key].update(values)

        elif keys_value_type[0] == dict:
            # Extract all dictionaries by key and enter in recursion
            dicts_to_merge = list(filter(lambda obj: obj != None, [dictionary.get(key, None) for dictionary in dictionaries]))
            merged_dict[key] = DictHelper.merge_dictionaries(*dicts_to_merge)

        else:
            # if override => get value from last dictionary else make a list of all values
            values = list(filter(lambda obj: obj != None, [dictionary.get(key, None) for dictionary in dictionaries]))
            merged_dict[key] = values[-1] if override else values

    return dict(merged_dict)



if __name__ == '__main__':
  d1 = {'aaaaaaaaa': ['to short', 'to long'], 'bbbbb': ['to short', 'to long'], "cccccc": ["the is a test"]}
  d2 = {'aaaaaaaaa': ['field is not a bool'], 'bbbbb': ['field is not a bool']}
  d3 = {'aaaaaaaaa': ['filed is not a string', "to short"], 'bbbbb': ['field is not an integer']}
  print(DictHelper.merge_dictionaries(d1, d2, d3))

  d4 = {"a": {"x": 1, "y": 2, "z": 3, "d": {"x1": 10}}}
  d5 = {"a": {"x": 10, "y": 20, "d": {"x2": 20}}}
  print(DictHelper.merge_dictionaries(d4, d5))

Production:

{'bbbbb': {'to long', 'field is not an integer', 'to short', 'field is not a bool'}, 
'aaaaaaaaa': {'to long', 'to short', 'filed is not a string', 'field is not a bool'}, 
'cccccc': {'the is a test'}}

{'a': {'y': 20, 'd': {'x1': 10, 'x2': 20}, 'z': 3, 'x': 10}}

Bien que ce code puisse répondre à la question, fournir un contexte supplémentaire concernant la raison et / ou la manière dont ce code répond à la question améliore sa valeur à long terme.
xiawi

Je pense que c'est une implémentation générique de la fusion d'un ou plusieurs dictionnaires imbriqués en tenant compte du type des objets qui seront marqués
Dorcioman

0

jetez un oeil au toolzpackage

import toolz
dict1={1:{"a":"A"},2:{"b":"B"}}
dict2={2:{"c":"C"},3:{"d":"D"}}
toolz.merge_with(toolz.merge,dict1,dict2)

donne

{1: {'a': 'A'}, 2: {'b': 'B', 'c': 'C'}, 3: {'d': 'D'}}

0

La fonction suivante fusionne b en a.

def mergedicts(a, b):
    for key in b:
        if isinstance(a.get(key), dict) or isinstance(b.get(key), dict):
            mergedicts(a[key], b[key])
        else:
            a[key] = b[key]
    return a

0

Et juste une autre légère variation:

Voici une fonction de mise à jour approfondie basée sur un ensemble python3 pur. Il met à jour les dictionnaires imbriqués en parcourant un niveau à la fois et s'appelle lui-même pour mettre à jour chaque niveau suivant de valeurs de dictionnaire:

def deep_update(dict_original, dict_update):
    if isinstance(dict_original, dict) and isinstance(dict_update, dict):
        output=dict(dict_original)
        keys_original=set(dict_original.keys())
        keys_update=set(dict_update.keys())
        similar_keys=keys_original.intersection(keys_update)
        similar_dict={key:deep_update(dict_original[key], dict_update[key]) for key in similar_keys}
        new_keys=keys_update.difference(keys_original)
        new_dict={key:dict_update[key] for key in new_keys}
        output.update(similar_dict)
        output.update(new_dict)
        return output
    else:
        return dict_update

Un exemple simple:

x={'a':{'b':{'c':1, 'd':1}}}
y={'a':{'b':{'d':2, 'e':2}}, 'f':2}

print(deep_update(x, y))
>>> {'a': {'b': {'c': 1, 'd': 2, 'e': 2}}, 'f': 2}

0

Et une autre réponse?!? Celui-ci évite également les mutations / effets secondaires:

def merge(dict1, dict2):
    output = {}

    # adds keys from `dict1` if they do not exist in `dict2` and vice-versa
    intersection = {**dict2, **dict1}

    for k_intersect, v_intersect in intersection.items():
        if k_intersect not in dict1:
            v_dict2 = dict2[k_intersect]
            output[k_intersect] = v_dict2

        elif k_intersect not in dict2:
            output[k_intersect] = v_intersect

        elif isinstance(v_intersect, dict):
            v_dict2 = dict2[k_intersect]
            output[k_intersect] = merge(v_intersect, v_dict2)

        else:
            output[k_intersect] = v_intersect

    return output
dict1 = {1:{"a":{"A"}}, 2:{"b":{"B"}}}
dict2 = {2:{"c":{"C"}}, 3:{"d":{"D"}}}
dict3 = {1:{"a":{"A"}}, 2:{"b":{"B"},"c":{"C"}}, 3:{"d":{"D"}}}

assert dict3 == merge(dict1, dict2)
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.