Bagaimana cara menggabungkan kamus kamus?


129

Saya perlu menggabungkan beberapa kamus, berikut ini contohnya:

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

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

Dengan A B Cdan Dmenjadi daun pohon, suka{"info1":"value", "info2":"value2"}

Ada tingkat (kedalaman) kamus yang tidak diketahui, bisa jadi {2:{"c":{"z":{"y":{C}}}}}

Dalam kasus saya ini merupakan struktur direktori / file dengan node menjadi dokumen dan meninggalkan file.

Saya ingin menggabungkan mereka untuk memperoleh:

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

Saya tidak yakin bagaimana saya bisa melakukannya dengan mudah dengan Python.


Apa yang Anda inginkan untuk kedalaman kamus Anda yang sewenang-wenang? Apakah Anda ingin ydiratakan ke ctingkat atau apa? Contoh Anda tidak lengkap.
agf

Lihat kelas NestedDict saya di sini: stackoverflow.com/a/16296144/2334951 Ia mengelola struktur kamus bersarang seperti penggabungan & lainnya.
SzieberthAdam

3
Peringatan untuk semua orang yang mencari solusi: Pertanyaan ini hanya tentang diktat bersarang. Sebagian besar jawaban tidak menangani kasus yang lebih rumit dari daftar dicts dalam struktur dengan benar. Jika Anda memerlukan ini, coba jawaban @Osiloke di bawah ini: stackoverflow.com/a/25270947/1431660
SHernandez


Jawaban:


143

ini sebenarnya cukup rumit - terutama jika Anda ingin pesan kesalahan yang berguna ketika ada hal-hal yang tidak konsisten, sementara menerima entri duplikat tapi konsisten dengan benar (sesuatu yang tidak ada jawaban lain di sini ....)

dengan asumsi Anda tidak memiliki banyak entri, fungsi rekursif paling mudah:

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

perhatikan bahwa ini bermutasi a- isi bditambahkan a(yang juga dikembalikan). jika Anda ingin tetap aAnda bisa menyebutnya seperti merge(dict(a), b).

agf menunjukkan (di bawah) bahwa Anda mungkin memiliki lebih dari dua dikt, dalam hal ini Anda dapat menggunakan:

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

di mana semuanya akan ditambahkan ke dict1.

[note - saya mengedit jawaban awal saya untuk mengubah argumen pertama; yang membuat "mengurangi" lebih mudah dijelaskan]

ps di python 3, Anda juga perlu from functools import reduce


1
Anda kemudian dapat menempel ini di dalam reduceatau loop yang setara untuk bekerja dengan angka dicts sembarang dari dua. Namun, saya tidak yakin ini melakukan apa yang dia inginkan (dia tidak jelas), Anda berakhir dengan 2: {'c': {'z': {'y': {'info1': 'value', 'info2': 'value2'}}}, 'b': {'info1': 'value', 'info2': 'value2'}}contoh kedua, saya tidak yakin apakah dia ingin zdan ydiratakan atau tidak?
agf

1
mereka adalah struktur direktori jadi saya tidak berpikir / dia ingin sesuatu yang rata? oh, maaf, ketinggalan "kamus berganda". ya, mengurangi akan bagus. akan menambahkan itu.
andrew cooke

Ini tepat seperti yang saya inginkan! Maaf saya tidak cukup jelas ... Saya pikir saya baik-baik saja dengan Python, sepertinya tidak: - / Saya memerlukan fungsi rekursif karena diktat bersarang, yang ini berfungsi dan saya bisa memahaminya :) Saya tidak tampaknya dapat membuatnya bekerja dengan mengurangi ...
fdhex

2
Bagi siapa pun dengan daftar sebagai tingkat bersarang akhir di bawah dicts, Anda dapat melakukan ini bukan untuk membesarkan kesalahan untuk menggabungkan dua daftar: a[key] = a[key] + b[key]. Terima kasih atas jawabannya.
kevinmicke

1
> jika Anda ingin menyimpan, Anda bisa menyebutnya seperti gabungan (dict (a), b) Perhatikan bahwa dict bersarang masih akan dimutasi. Untuk menghindari ini, gunakan copy.deepcopy.
rcorre

31

Berikut cara mudah untuk melakukannya menggunakan generator:

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

Ini mencetak:

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

jika Anda ingin menyimpan tema generator, Anda dapat mem-chain (dict1.keys (), dict2.keys ())
andrew cooke

Bukankah itu akan mendapatkan kunci duplikat?
jterrace

Yang ini tampaknya melakukan pekerjaan, setidaknya pada set data saya, tetapi karena saya tidak pernah mengerti hasil dan generator, saya cukup banyak kehilangan mengapa, tapi saya akan mencoba sedikit lebih keras, mungkin berguna!
fdhex

ah, ya, itu akan mendapatkan kunci duplikat. Anda masih harus membungkusnya dalam set, maaf.
andrew cooke

2
Saya menemukan ini sangat membantu. Tetapi yang paling baik adalah membiarkan fungsi untuk menyelesaikan konflik sebagai parameter.
mentatkgs

25

Salah satu masalah dengan pertanyaan ini adalah bahwa nilai-nilai dict dapat berupa data yang kompleks dan sewenang-wenang. Berdasarkan jawaban ini dan lainnya, saya menemukan kode ini:

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

Kasus penggunaan saya menggabungkan file YAML di mana saya hanya harus berurusan dengan subset dari tipe data yang mungkin. Karenanya saya dapat mengabaikan tupel dan objek lainnya. Bagi saya logika penggabungan yang masuk akal artinya

  • ganti skalar
  • tambahkan daftar
  • gabungkan dikte dengan menambahkan kunci yang hilang dan memperbarui kunci yang ada

Segala sesuatu yang lain dan yang tak terduga menghasilkan kesalahan.


1
Fantastis. Bekerja dengan baik pada json dumps juga. Baru saja menghapus penanganan kesalahan. (Menjadi malas, dapat melakukan yang tepat untuk json saya yakin)
dgBP

3
urutan "isinstance" dapat diganti tanpa / isinstance(a, (str, unicode, int, long, float))bukan?
simahawk

12

Kamus kamus bergabung

Karena ini adalah pertanyaan kanonik (terlepas dari non-generalisasi tertentu) saya menyediakan pendekatan Pythonic kanonik untuk menyelesaikan masalah ini.

Kasus paling sederhana: "daun adalah dict bersarang yang berakhir dict kosong":

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

Ini adalah kasus paling sederhana untuk rekursi, dan saya akan merekomendasikan dua pendekatan naif:

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

Saya yakin saya lebih suka yang kedua daripada yang pertama, tetapi perlu diingat bahwa keadaan awal dari yang pertama harus dibangun kembali dari asalnya. Inilah penggunaannya:

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

Kasing Kompleks: "daun dari jenis lain:"

Jadi jika mereka berakhir dicts, itu adalah kasus sederhana penggabungan akhir dicts. Jika tidak, itu tidak sepele. Jika string, bagaimana Anda menggabungkannya? Set dapat diperbarui dengan cara yang sama, sehingga kami dapat memberikan perawatan itu, tetapi kami kehilangan urutan penggabungannya. Jadi apakah pesanan itu penting?

Jadi sebagai pengganti informasi lebih lanjut, pendekatan yang paling sederhana adalah memberi mereka perlakuan pembaruan standar jika kedua nilai bukan dicts: yaitu nilai dict kedua akan menimpa yang pertama, bahkan jika nilai dict kedua adalah Tidak ada dan nilai pertama adalah dict dengan banyak info.

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

Dan sekarang

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

kembali

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

Aplikasi untuk pertanyaan awal:

Saya harus menghapus kurung kurawal di sekitar huruf dan menempatkannya dalam tanda kutip tunggal untuk ini menjadi Python yang sah (kalau tidak mereka akan menetapkan literal dalam Python 2.7+) serta menambahkan kurung kurawal yang hilang:

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

dan rec_merge(dict1, dict2)sekarang kembali:

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

Yang cocok dengan hasil yang diinginkan dari pertanyaan asli (setelah mengubah, misalnya {A}ke 'A'.)


10

Berdasarkan cooke @andrew. Versi ini menangani daftar dikte bersarang dan juga memungkinkan opsi untuk memperbarui nilai

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
Terima kasih, ini sangat membantu. Saya memiliki daftar dicts di struktur saya sepanjang waktu, solusi lain tidak dapat menggabungkan ini dengan benar.
SHernandez

7

Prosedur rekursif sederhana ini akan menggabungkan satu kamus ke kamus lain sambil meng-override kunci yang saling bertentangan:

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

Keluaran:

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

7

Berdasarkan jawaban dari @andrew cooke. Ini mengurus daftar bersarang dengan cara yang lebih baik.

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 dan simetris. +1 untuk penanganan daftar :)
vdwees

6

Jika Anda memiliki tingkat kamus yang tidak diketahui, maka saya menyarankan fungsi rekursif:

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

Gambaran

Pendekatan berikut membagi masalah penggabungan dalam dikt ke dalam:

  1. Fungsi gabungan dangkal yang diparameterisasi merge(f)(a,b) yang menggunakan fungsi funtuk menggabungkan dua dicts adanb

  2. Fungsi merger rekursif f untuk digunakan bersamamerge


Penerapan

Fungsi untuk menggabungkan dua dikte (tidak bersarang) dapat ditulis dalam banyak cara. Saya pribadi suka

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

Cara yang baik untuk mendefinisikan fungsi merger rekursif yang tepat fadalah dengan menggunakan multipledispatch yang memungkinkan untuk mendefinisikan fungsi yang mengevaluasi sepanjang jalur yang berbeda tergantung pada jenis argumen mereka.

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)

Contoh

Untuk menggabungkan dua dikte bersarang cukup gunakan merge(f)misalnya:

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

Catatan:

Keuntungan dari pendekatan ini adalah:

  • Fungsi ini dibangun dari fungsi yang lebih kecil yang masing-masing melakukan satu hal yang membuat kode lebih mudah untuk dipertimbangkan dan diuji

  • Perilaku ini bukan hard-coded tetapi dapat diubah dan diperluas sesuai kebutuhan yang meningkatkan penggunaan kembali kode (lihat contoh di bawah).


Kustomisasi

Beberapa jawaban juga dianggap sebagai dikte yang berisi daftar, misalnya dikte lain (yang berpotensi bersarang). Dalam hal ini orang mungkin ingin memetakan daftar dan menggabungkannya berdasarkan posisi. Ini dapat dilakukan dengan menambahkan definisi lain ke fungsi merger f:

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

4

Jika seseorang menginginkan yang lain pendekatan untuk masalah ini, inilah solusi saya.

Kebajikan : pendek, deklaratif, dan fungsional dalam gaya (rekursif, tidak ada mutasi).

Kerugian Potensial : Ini mungkin bukan gabungan yang Anda cari. Konsultasikan dengan docstring untuk semantik.

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
        }

Jawaban yang sangat menarik, terima kasih telah membagikannya. Sintaks apa yang Anda gunakan setelah pernyataan kembali? Saya tidak terbiasa dengan itu.
dev_does_software

4

Anda dapat mencoba mergedeep .


Instalasi

$ pip3 install mergedeep

Pemakaian

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

Untuk daftar opsi lengkap, lihat dokumen !


3

Ada sedikit masalah dengan andrew cookes menjawab: Dalam beberapa kasus ini memodifikasi argumen kedua bketika Anda memodifikasi dict yang dikembalikan. Khususnya karena baris ini:

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

Jika b[key]a dict, itu hanya akan ditugaskan a, yang berarti setiap modifikasi berikutnya yang dictakan mempengaruhi keduanya adan 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)

Untuk memperbaiki ini, saluran harus diganti dengan ini:

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

Dimana clone_dict:

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

Masih. Ini jelas tidak memperhitungkan list, setdan hal-hal lain, tapi saya harap ini menggambarkan perangkap ketika mencoba untuk bergabungdicts .

Dan demi kelengkapannya, ini adalah versi saya, di mana Anda dapat memberikannya beberapa 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, {})

Mengapa tidak deepcopybukan clone_dict?
Armando Pérez Marqués

1
Karena stdlib python sangat besar dan megah! Saya tidak tahu ini ada - plus itu adalah hal kecil yang menyenangkan untuk kode :-)
andsens

2

Versi fungsi ini akan menjelaskan N jumlah kamus, dan hanya kamus - tidak ada parameter yang tidak patut yang dapat dilewati, atau akan meningkatkan TypeError. Menggabungkan itu sendiri bertanggung jawab atas konflik utama, dan alih-alih menimpa data dari kamus lebih jauh ke bawah rantai gabungan, itu menciptakan serangkaian nilai dan menambahkannya; tidak ada data yang hilang.

Ini mungkin bukan yang paling efisien di halaman, tapi itu yang paling menyeluruh dan Anda tidak akan kehilangan informasi apa pun ketika Anda menggabungkan 2 ke N Anda.

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

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


2

Karena dictviews mendukung operasi yang ditetapkan, saya dapat sangat menyederhanakan jawaban 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])))

Setiap upaya untuk menggabungkan dict dengan non dict (secara teknis, objek dengan metode 'kunci' dan objek tanpa metode 'kunci') akan meningkatkan AttributeError. Ini termasuk panggilan awal ke fungsi dan panggilan rekursif. Ini persis apa yang saya inginkan jadi saya meninggalkannya. Anda dapat dengan mudah menangkap AttributeErrors yang dilemparkan oleh panggilan rekursif dan kemudian menghasilkan nilai apa pun yang Anda inginkan.


2

Pendek-n-manis:

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]

Ini berfungsi seperti (dan sedang dibangun di atas) dict.updatemetode Python . Itu kembali None(Anda selalu dapat menambahkan return djika Anda mau) karena pembaruan dict ddi tempat. Kunci dalam vakan menimpa setiap kunci yang ada did (itu tidak mencoba untuk menafsirkan konten dict).

Ini juga akan berfungsi untuk pemetaan ("dict-like") lainnya.


1

Kode akan bergantung pada aturan Anda untuk menyelesaikan konflik gabungan, tentu saja. Berikut adalah versi yang dapat mengambil sejumlah argumen arbitrer dan menggabungkannya secara rekursif ke kedalaman arbitrer, tanpa menggunakan mutasi objek apa pun. Itu menggunakan aturan berikut untuk menyelesaikan konflik gabungan:

  • kamus diutamakan daripada nilai-nilai non-dict ( {"foo": {...}}diutamakan{"foo": "bar"} )
  • argumen kemudian didahulukan dari argumen sebelumnya (jika Anda menggabungkan {"a": 1}, {"a", 2}dan {"a": 3}dalam rangka, hasilnya akan {"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

Saya memiliki dua kamus ( adan b) yang masing-masing dapat berisi sejumlah kamus bersarang. Saya ingin menggabungkan mereka secara rekursif, dengan bmengambil alih prioritasa .

Mempertimbangkan kamus bersarang sebagai pohon, yang saya inginkan adalah:

  • Untuk memperbarui asehingga setiap jalur ke setiap daun bakan diwakili dalama
  • Untuk menimpa sub pohon dari ajika daun ditemukan di jalur yang sesuai dib
    • Pertahankan yang lain bahwa semua bsimpul daun tetap daun.

Jawaban yang ada agak rumit untuk seleraku dan meninggalkan beberapa detail di rak. Saya meretas bersama-sama berikut ini, yang lulus tes unit untuk set data saya.

  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

Contoh (diformat untuk kejelasan):

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

Jalur byang perlu dipertahankan adalah:

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

a memiliki jalur unik dan tidak bertentangan:

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

jadi mereka masih terwakili di peta gabungan.


1

Saya punya solusi berulang - bekerja jauh lebih baik dengan dikte besar & banyak di antaranya (misalnya jsons dll):

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

perhatikan bahwa ini akan menggunakan nilai dalam d2 untuk mengesampingkan d1, jika keduanya bukan keduanya. (Sama seperti pythondict.update() )

beberapa tes:

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

Saya telah menguji dengan sekitar ~ 1200 dicts - metode ini memakan waktu 0,4 detik, sedangkan solusi rekursif mengambil ~ 2,5 detik.


0

Ini akan membantu menggabungkan semua item dict2menjadi 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]

Silakan mengujinya dan beri tahu kami apakah ini yang Anda inginkan.

EDIT:

Solusi yang disebutkan di atas hanya menggabungkan satu level, tetapi dengan benar memecahkan contoh yang diberikan oleh OP. Untuk menggabungkan beberapa level, rekursi harus digunakan.


1
Dia punya kedalaman sewenang-wenang bersarang
AGF

Itu dapat ditulis ulang hanya sebagai for k,v in dict2.iteritems(): dict1.setdefault(k,{}).update(v). Tapi seperti yang ditunjukkan @agf, ini tidak menggabungkan dicts yang bersarang.
Shawn Chin

@agf: Benar, jadi sepertinya OP membutuhkan solusi menggunakan pengulangan. Berkat kamus fakta bisa berubah, ini seharusnya cukup mudah dilakukan. Tetapi saya pikir pertanyaannya tidak cukup spesifik untuk mengatakan apa yang harus terjadi ketika kami menemukan tempat-tempat dengan tingkat kedalaman yang berbeda (mis. Mencoba untuk bergabung {'a':'b'}dengan {'a':{'c':'d'}).
Tadeck

0

Saya telah menguji solusi Anda dan memutuskan untuk menggunakan yang ini dalam proyek saya:

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

Melewati fungsi sebagai parameter adalah kunci untuk memperluas solusi jterrace agar berperilaku seperti semua solusi rekursif lainnya.


0

Cara termudah yang bisa saya pikirkan adalah:

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

Keluaran:

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

0

Saya punya solusi lain yang sedikit berbeda di sini:

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

Secara default ini menyelesaikan konflik yang mendukung nilai-nilai dari dikt kedua, tetapi Anda dapat dengan mudah menimpanya, dengan beberapa sihir Anda bahkan dapat membuang pengecualian darinya. :)


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

hei di sana saya juga punya masalah yang sama tetapi saya memikirkan solusi dan saya akan mempostingnya di sini, kalau-kalau ini juga berguna untuk yang lain, pada dasarnya menggabungkan kamus yang bersarang dan juga menambahkan nilai-nilai, bagi saya saya perlu menghitung beberapa probabilitas jadi ini satu bekerja dengan baik:

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

dengan menggunakan metode di atas kita dapat menggabungkan:

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

dan ini akan menjadi: {'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 }}

perhatikan juga perubahan di sini:

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

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

jangan lupa juga menambahkan impor untuk salinan:

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

Keluaran:

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

Sementara kode ini dapat menjawab pertanyaan, memberikan konteks tambahan tentang mengapa dan / atau bagaimana kode ini menjawab pertanyaan meningkatkan nilai jangka panjangnya.
xiawi

Saya pikir ini adalah implementasi umum dari penggabungan satu atau lebih kamus bersarang dengan mempertimbangkan jenis objek yang akan di-marged
Dorcioman

0

lihat toolzpaketnya

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

memberi

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

0

Fungsi berikut menggabungkan b menjadi 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

Dan hanya sedikit variasi lain:

Berikut ini adalah fungsi pembaruan mendalam set python3 murni. Ini memperbarui kamus bersarang dengan perulangan melalui satu tingkat pada satu waktu dan menyebut dirinya untuk memperbarui setiap tingkat nilai kamus berikutnya:

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

Contoh sederhana:

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

Bagaimana dengan jawaban lain?!? Yang ini juga menghindari mutasi / efek samping:

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)
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.