Jak połączyć słowniki słowników?

135

Muszę scalić wiele słowników, oto co mam na przykład:

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

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

Z A B Ci Dbędąc liśćmi drzewa, jak{"info1":"value", "info2":"value2"}

Możliwe, że istnieje nieznany poziom (głębokość) słowników {2:{"c":{"z":{"y":{C}}}}}

W moim przypadku reprezentuje strukturę katalogów / plików z węzłami będącymi dokumentami i pozostawiając jako pliki.

Chcę je połączyć, aby uzyskać:

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

Nie jestem pewien, jak mógłbym to łatwo zrobić w Pythonie.

fdhex
źródło
Czego oczekujesz od swojej dowolnej głębi słowników? Chcesz yspłaszczyć do cpoziomu czy co? Twój przykład jest niekompletny.
agf
Sprawdź moją klasę NestedDict tutaj: stackoverflow.com/a/16296144/2334951 Zarządza zagnieżdżonymi strukturami słownika, takimi jak scalanie i nie tylko.
SzieberthAdam
3
Ostrzeżenie dla wszystkich szukających rozwiązań: to pytanie dotyczy tylko poleceń zagnieżdżonych. Większość odpowiedzi nie radzi sobie poprawnie z bardziej skomplikowanym przypadkiem list dykt w strukturze. Jeśli potrzebujesz tego, wypróbuj odpowiedź @Osiloke poniżej: stackoverflow.com/a/25270947/1431660
SHernandez
Zobacz także: python dpath merge
dreftymac
Zobacz także: scalanie wielu słowników
dreftymac

Odpowiedzi:

148

jest to w rzeczywistości dość trudne - szczególnie jeśli chcesz otrzymać przydatny komunikat o błędzie, gdy rzeczy są niespójne, jednocześnie poprawnie akceptując zduplikowane, ale spójne wpisy (coś, czego nie robi żadna inna odpowiedź ...)

zakładając, że nie masz dużej liczby wpisów, funkcja rekurencyjna jest najłatwiejsza:

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

zauważ, że to mutuje a- zawartość bjest dodawana do a(która również jest zwracana). jeśli chcesz zatrzymać, amożesz to nazwać tak merge(dict(a), b).

agf wskazał (poniżej), że możesz mieć więcej niż dwie dykty, w takim przypadku możesz użyć:

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

gdzie wszystko zostanie dodane do dict1.

[uwaga - zredagowałem moją początkową odpowiedź, aby zmienić pierwszy argument; to sprawia, że ​​„redukcja” jest łatwiejsza do wyjaśnienia]

ps w pythonie 3, będziesz również potrzebować from functools import reduce

Andrew Cooke
źródło
1
Następnie możesz umieścić to w reducepętli lub równoważnej, aby pracować z dowolną liczbą dicts zamiast dwóch. Jednak nie jestem pewien, czy to robi to, czego chce (nie był jasny), kończysz z 2: {'c': {'z': {'y': {'info1': 'value', 'info2': 'value2'}}}, 'b': {'info1': 'value', 'info2': 'value2'}}jego drugim przykładem, nie jestem pewien, czy chce zi yspłaszczony, czy nie?
agf
1
są to struktury katalogów, więc nie sądzę, żeby on chciał coś spłaszczonego? och, przepraszam, brakowało „wielu słowników”. tak, redukcja byłaby dobra. dodam to.
andrew cooke
Robi dokładnie to, czego chciałem! Przepraszam, że nie byłem wystarczająco jasny ... Myślałem, że jestem w porządku z Pythonem, wydaje się, że nie: - / Potrzebowałem funkcji rekurencyjnej z powodu zagnieżdżonych dykt, ta działa i rozumiem to :) Nie wydaje się, że jest w stanie działać z redukcją ...
fdhex
2
Dla każdego z listy jako ostatecznego poziomu zagnieżdżonego pod dicts, można zrobić to zamiast podniesienia błąd, aby złączyć dwie listy: a[key] = a[key] + b[key]. Dzięki za pomocną odpowiedź.
kevinmicke
1
> jeśli chcesz zachować a, możesz nazwać to jak merge (dict (a), b) Zauważ, że zagnieżdżone dykty nadal będą mutowane. Aby tego uniknąć, użyj copy.deepcopy.
rcorre
34

Oto prosty sposób na zrobienie tego za pomocą generatorów:

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

To drukuje:

{1: {'a': 'A'}, 2: {'c': 'C', 'b': 'B'}, 3: {'d': 'D'}}
jterrace
źródło
jeśli chcesz zachować motyw generatora, możesz połączyć łańcuchami (dict1.keys (), dict2.keys ())
andrew cooke
Czy nie dałoby to zduplikowanych kluczy?
jterrace
Ten wydaje się działać, przynajmniej na moim zestawie danych, ale ponieważ nigdy dobrze nie rozumiałem wydajności i generatorów, nie rozumiem, dlaczego, ale spróbuję trochę bardziej, może się przydać!
fdhex
Ach, tak, dostanie zduplikowane klucze. Nadal musiałbyś zapakować go w zestaw, przepraszam.
andrew cooke
2
Uważam, że to szczególnie pomocne. Ale najmilsze byłoby zezwolenie funkcji na rozwiązywanie konfliktów jako parametr.
mentatkgs
25

Jednym z problemów związanych z tym pytaniem jest to, że wartościami dyktowania mogą być dowolnie złożone fragmenty danych. Na podstawie tych i innych odpowiedzi wymyśliłem ten kod:

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

Mój przypadek użycia to scalanie plików YAML, gdzie mam do czynienia tylko z podzbiorem możliwych typów danych. Dlatego mogę ignorować krotki i inne obiekty. Dla mnie rozsądna logika scalania oznacza

  • zastąpić skalary
  • dołącz listy
  • łączyć dykty, dodając brakujące klucze i aktualizując istniejące klucze

Wszystko inne i nieprzewidziane prowadzi do błędu.

Schlomo
źródło
1
Fantastyczny. Działa również dobrze na zrzutach JSON. Właśnie usunąłem obsługę błędów. (Jestem leniwy, jestem pewien, że mogę zrobić odpowiednie dla json)
dgBP
3
sekwencja „isinstance” może zostać zastąpiona bez isinstance(a, (str, unicode, int, long, float))niej?
simahawk
12

Możesz spróbować połączyć się .


Instalacja

$ pip3 install mergedeep

Stosowanie

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

Pełną listę opcji znajdziesz w dokumentacji !

Travis Clarke
źródło
12

Słowniki słowników łączą się

Ponieważ jest to kwestia kanoniczna (pomimo pewnych nieogólności), przedstawiam kanoniczne podejście Pythona do rozwiązania tego problemu.

Najprostszy przypadek: „liście to zagnieżdżone dykty, które kończą się pustymi słowami”:

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

To najprostszy przypadek rekurencji i zalecałbym dwa naiwne podejścia:

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

Wydaje mi się, że wolałbym drugi od pierwszego, ale pamiętaj, że pierwotny stan pierwszego musiałby zostać odbudowany od jego pochodzenia. Oto zastosowanie:

>>> 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': {}}}}

Przypadek złożony: „liście są dowolnego innego typu:”

Więc jeśli kończą się dyktami, jest to prosty przypadek połączenia pustych dykt na końcu. Jeśli nie, to nie jest takie trywialne. Jeśli ciągi, jak je scalić? Zestawy mogą być aktualizowane w podobny sposób, więc możemy poddać to obróbce, ale tracimy kolejność, w jakiej zostały połączone. Więc czy porządek ma znaczenie?

Zatem zamiast większej ilości informacji, najprostszym podejściem będzie zapewnienie im standardowego traktowania aktualizacji, jeśli obie wartości nie są dyktami: tj. Wartość drugiego dyktu zastąpi pierwszą, nawet jeśli wartość drugiej dyktu to None, a pierwsza to a dyktować z dużą ilością informacji.

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

from collections.abc 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():
        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

I teraz

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

zwroty

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

Zastosowanie do pierwotnego pytania:

Musiałem usunąć nawiasy klamrowe wokół liter i umieścić je w pojedynczych cudzysłowach, aby był to legalny Python (w przeciwnym razie byłyby ustawionymi literałami w Pythonie 2.7+), a także dodać brakujący nawias:

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

a rec_merge(dict1, dict2)teraz zwraca:

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

Który pasuje do pożądanego wyniku pierwotnego pytania (po zmianie, np {A}do 'A'.)

Aaron Hall
źródło
11

Na podstawie @andrew cooke. Ta wersja obsługuje zagnieżdżone listy dykt, a także umożliwia aktualizację wartości

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
Osiloke
źródło
1
Dzięki, to jest bardzo pomocne. Cały czas mam w swoich strukturach listy dykt, inne rozwiązania nie potrafią tego poprawnie scalić.
SHernandez
8

Ta prosta procedura rekurencyjna połączy jeden słownik w inny, nadpisując sprzeczne klucze:

#!/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"}}))

Wynik:

{1: {'a': 'A'}, 2: {'c': 'C', 'b': 'B'}, 3: {'d': 'D'}}
{1: {'a': 'A'}, 2: {'b': 'C'}}
Michael Spector
źródło
7

Na podstawie odpowiedzi udzielonych przez @andrew cooke. Lepiej dba o listy zagnieżdżone.

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]
Vikas Kumar
źródło
intuicyjny i symetryczny. +1 do obsługi listy :)
vdwees
6

Jeśli masz nieznany poziom słowników, proponuję funkcję rekurencyjną:

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
Spencer Rathbun
źródło
5

Przegląd

Poniższe podejście dzieli problem głębokiego połączenia dykt na:

  1. Sparametryzowana funkcja płytkiego scalania, merge(f)(a,b)która używa funkcji fdo scalenia dwóch dykt aib

  2. Rekursywna funkcja scalająca, fktóra ma być używana razem zmerge


Realizacja

Funkcję do łączenia dwóch (nie zagnieżdżonych) dykt można zapisać na wiele sposobów. Osobiście lubię

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

Dobrym sposobem na zdefiniowanie odpowiedniej funkcji scalającej rekurencyjnej fjest użycie multipledispatch, która pozwala zdefiniować funkcje obliczające według różnych ścieżek w zależności od typu ich argumentów.

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)

Przykład

Aby połączyć dwa zagnieżdżone polecenia, po prostu użyj merge(f)np .:

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

Uwagi:

Zalety tego podejścia to:

  • Funkcja jest zbudowana z mniejszych funkcji, z których każda wykonuje jedną rzecz, co ułatwia wnioskowanie i testowanie kodu

  • To zachowanie nie jest zakodowane na stałe, ale można je zmienić i rozszerzyć w razie potrzeby, co poprawia ponowne wykorzystanie kodu (patrz przykład poniżej).


Dostosowywanie

W niektórych odpowiedziach uwzględniono również dykty zawierające listy, np. Inne (potencjalnie zagnieżdżone) dykty. W takim przypadku można zmapować listy i połączyć je na podstawie pozycji. Można to zrobić, dodając kolejną definicję do funkcji łączenia f:

import itertools
@dispatch(list, list)
def f(a,b):
    return [merge(f)(*arg) for arg in itertools.zip_longest(a, b)]
Sascha
źródło
4

Na wypadek, gdyby ktoś chciał innego podejścia do tego problemu, oto moje rozwiązanie.

Cnoty : krótkie, deklaratywne i funkcjonalne w stylu (rekurencyjne, bez mutacji).

Potencjalna wada : może to nie być połączenie, którego szukasz. Sprawdź dokumentację dotyczącą semantyki.

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
        }
David Schneider
źródło
Bardzo interesująca odpowiedź, dziękuję za udostępnienie. Jakiej składni użyłeś po instrukcji return? Nie znam tego.
dev_does_software
3

Jest mały problem z odpowiedzią Andrew cookes: W niektórych przypadkach modyfikuje drugi argument, bgdy modyfikujesz zwrócony dykt. W szczególności to z powodu tej linii:

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

Jeśli b[key]jest a dict, zostanie po prostu przypisany do a, co oznacza, że ​​wszelkie późniejsze modyfikacje dictbędą miały wpływ zarówno na, jak ai 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)

Aby to naprawić, wiersz musiałby zostać zastąpiony tym:

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

Gdzie clone_dictjest:

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

Nadal. To oczywiście nie bierze pod uwagę list, seti inne rzeczy, ale mam nadzieję, że ilustruje pułapek podczas próby scalenia dicts.

A dla kompletności, oto moja wersja, w której możesz podać ją wielokrotnie 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, {})
andsens
źródło
Dlaczego nie deepcopyzamiast clone_dict?
Armando Pérez Marqués
1
Ponieważ stdlib pythona jest cholernie ogromny i wspaniały! Nie miałem pojęcia, że ​​to istnieje - a dodatkowo kodowanie było fajną rzeczą :-)
andsens
2

Ta wersja funkcji będzie uwzględniać liczbę N słowników i tylko słowniki - nie można przekazać żadnych niewłaściwych parametrów lub spowoduje to zgłoszenie błędu TypeError. Samo scalanie uwzględnia konflikty kluczy i zamiast nadpisywać dane ze słownika w dalszej części łańcucha scalania, tworzy zestaw wartości i dołącza do niego; żadne dane nie zostaną utracone.

To może nie być najbardziej wydajne na stronie, ale jest najdokładniejsze i nie stracisz żadnych informacji, gdy połączysz swoje 2 z N.

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

wyjście: {1: [1, 2], 2: {1: 2, 3: 1}, 4: 4}

blakev
źródło
2

Ponieważ dictviews obsługuje operacje na zbiorach, mogłem znacznie uprościć odpowiedź 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])))

Każda próba połączenia dict z non dict (technicznie rzecz biorąc, obiekt z metodą „keys” i obiekt bez metody „keys”) spowoduje zgłoszenie błędu AttributeError. Obejmuje to zarówno początkowe wywołanie funkcji, jak i wywołania rekurencyjne. To jest dokładnie to, czego chciałem, więc zostawiłem to. Możesz łatwo złapać AttributeErrors rzucone przez wywołanie rekurencyjne, a następnie uzyskać dowolną wartość.

Guy Gangemi
źródło
2

Krótkie i słodkie:

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]

Działa to podobnie do dict.updatemetody Pythona (i jest na niej zbudowane) . Powraca None(zawsze możesz dodać, return djeśli wolisz), ponieważ aktualizuje dna miejscu. Keys in vnadpisze wszystkie istniejące klucze w d(nie próbuje zinterpretować zawartości dyktu).

Będzie również działać z innymi mapowaniami („podobnymi do dyktowania”).

Hans Bouwmeester
źródło
1

Kod będzie oczywiście zależał od twoich reguł rozwiązywania konfliktów scalania. Oto wersja, która może przyjąć dowolną liczbę argumentów i scala je rekurencyjnie na dowolną głębokość, bez użycia jakiejkolwiek mutacji obiektu. Używa następujących reguł do rozwiązywania konfliktów scalania:

  • słowniki mają pierwszeństwo przed wartościami niedozwolonymi ( {"foo": {...}}ma pierwszeństwo przed {"foo": "bar"})
  • później argumenty mają pierwszeństwo przed wcześniejszymi argumentami (jeśli połączyć {"a": 1}, {"a", 2}oraz {"a": 3}w porządku, wynik będzie {"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  
singingwolfboy
źródło
1

Miałem dwa słowniki ( ai b), z których każdy mógł zawierać dowolną liczbę zagnieżdżonych słowników. Chciałem je rekurencyjnie scalić, bmając pierwszeństwo przed a.

Biorąc pod uwagę zagnieżdżone słowniki jako drzewa, chciałem:

  • Aby zaktualizować a, aby każda ścieżka do każdego liścia bbyła reprezentowana wa
  • Aby nadpisać poddrzewa, ajeśli liść znajduje się w odpowiedniej ścieżce wb
    • Zachowaj niezmiennik, że wszystkie bwęzły liści pozostają liśćmi.

Istniejące odpowiedzi były trochę skomplikowane jak na mój gust i pozostawiły kilka szczegółów na półce. Zhakowałem razem następujące elementy, które pomyślnie przeszły testy jednostkowe dla mojego zestawu danych.

  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

Przykład (sformatowany dla przejrzystości):

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

Ścieżki, bktóre należało utrzymać, to:

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

a miał unikalne i niesprzeczne ścieżki:

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

więc są nadal reprezentowane na scalonej mapie.

mateor
źródło
1

spójrz na toolzpakiet

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

daje

{1: {'a': 'A'}, 2: {'b': 'B', 'c': 'C'}, 3: {'d': 'D'}}
user15964
źródło
1

Mam rozwiązanie iteracyjne - działa znacznie lepiej z dużymi dyktami i wieloma z nich (na przykład jsons itp.):

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

zwróć uwagę, że to użyje wartości z d2 do zastąpienia d1, na wypadek gdyby obie nie były dyktami. (tak samo jak Pythona dict.update())

kilka testów:

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

Testowałem z około 1200 dyktami - ta metoda zajęła 0,4 sekundy, a rozwiązanie rekurencyjne ~ 2,5 sekundy.

Alon Gouldman
źródło
0

Powinno to pomóc w łącząc wszystkie elementy od dict2do 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]

Przetestuj go i powiedz nam, czy tego właśnie chciałeś.

EDYTOWAĆ:

Powyższe rozwiązanie łączy tylko jeden poziom, ale poprawnie rozwiązuje przykład podany przez OP. Aby scalić wiele poziomów, należy użyć rekursji.

Tadeck
źródło
1
Ma dowolną głębokość zagnieżdżenia
agf
Można to przepisać po prostu jako for k,v in dict2.iteritems(): dict1.setdefault(k,{}).update(v). Ale jak zauważył @agf, nie powoduje to scalenia zagnieżdżonych poleceń.
Shawn Chin
@agf: Prawidłowo, więc wygląda na to, że OP potrzebuje rozwiązania wykorzystującego powtarzalność. Dzięki temu, że słowniki są zmienne, powinno to być dość łatwe do zrobienia. Ale myślę, że pytanie nie jest wystarczająco szczegółowe, aby powiedzieć, co powinno się stać, gdy wymyślimy miejsca o różnych poziomach głębokości (np. Próbując się {'a':'b'}z nimi połączyć {'a':{'c':'d'}).
Tadeck
0

Testowałem Twoje rozwiązania i zdecydowałem się użyć tego w moim projekcie:

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

Przekazywanie funkcji jako parametrów jest kluczem do rozszerzenia rozwiązania jterrace, aby zachowywało się jak wszystkie inne rozwiązania rekurencyjne.

mentatkgs
źródło
0

Najłatwiej przychodzi mi do głowy:

#!/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)

Wynik:

{1: {'a': 'A'}, 2: {'c': 'C', 'b': 'B'}, 3: {'d': 'D'}}
James
źródło
0

Mam tutaj inne nieco inne rozwiązanie:

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

Domyślnie rozwiązuje konflikty na korzyść wartości z drugiego dyktu, ale możesz to łatwo zmienić, z niektórymi czarami możesz nawet wyrzucić z niego wyjątki. :).

Slava
źródło
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()
wong steve
źródło
0

hej tam też miałem ten sam problem ale przyszło mi do głowy rozwiązanie i napiszę je tutaj na wypadek, gdyby przydało się to również innym, w zasadzie łącząc zagnieżdżone słowniki i dodając wartości, dla mnie musiałem obliczyć pewne prawdopodobieństwa, więc to jeden działał świetnie:

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

korzystając z powyższej metody możemy scalić:

cel = {'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} }

a to stanie się: {'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 }}

zwróć też uwagę na zmiany tutaj:

cel = {'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} }

nie zapomnij również dodać importu do kopiowania:

import copy
SlackSpace
źródło
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))

Wynik:

{'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}}
Dorcioman
źródło
Chociaż ten kod może odpowiedzieć na pytanie, zapewnia dodatkowy kontekst dotyczący tego, dlaczego i / lub jak ten kod odpowiada, poprawia jego długoterminową wartość.
xiawi
Myślę, że jest to ogólna implementacja łączenia jednego lub więcej zagnieżdżonych słowników, biorąc pod uwagę typ obiektów, które będą
zaznaczane
0

Następująca funkcja łączy b w 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
Ali Sadeghi Ardestani
źródło
0

I jeszcze jedna niewielka zmiana:

Oto funkcja głębokiej aktualizacji oparta na czystym zestawie Python3. Aktualizuje zagnieżdżone słowniki, przechodząc przez jeden poziom na raz i woła o aktualizację każdego kolejnego poziomu wartości słownika:

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

Prosty przykład:

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}
conmak
źródło
0

A może inna odpowiedź?!? Ten również pozwala uniknąć mutacji / skutków ubocznych:

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)
kemri
źródło