Як злити словники словників?


129

Мені потрібно об'єднати декілька словників, ось що у мене є, наприклад:

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

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

З A B Cі Dбути листям дерева, як{"info1":"value", "info2":"value2"}

Існує невідомий рівень (глибина) словників, це міг бути {2:{"c":{"z":{"y":{C}}}}}

У моєму випадку він представляє структуру каталогу / файлів, у яких вузли є документами, а листя - файлами.

Я хочу об'єднати їх для отримання:

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

Я не впевнений, як я міг це легко зробити з Python.


Що ви хочете для своєї довільної глибини словників? Хочете вирівняти yдо cрівня чи що? Ваш приклад неповний.
agf

Перевірте мій клас NestedDict тут: stackoverflow.com/a/16296144/2334951 Це керуючий гніздових словникових структур як злиття і багато іншого.
SzieberthAdam

3
Попередження для всіх, хто шукає рішення: Це питання стосується лише вкладених диктів. Більшість відповідей не обробляють складніший випадок списків диктів всередині структури належним чином. Якщо вам потрібно це спроба відповісти на @Osiloke нижче: stackoverflow.com/a/25270947/1431660
SHernandez

Дивіться також: python dpath merge
dreftymac

Відповіді:


143

це насправді досить складно - особливо якщо ви хочете корисне повідомлення про помилку, коли речі суперечать, при правильному прийнятті дублікатів, але послідовних записів (щось інше не відповідає тут ...)

припускаючи, що у вас немає величезної кількості записів, найпростіша рекурсивна функція:

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

зауважте, що це мутує a- вміст bдодається до a(який також повертається). якщо ви хочете зберегти, aви можете назвати це так merge(dict(a), b).

agf вказав (нижче), що у вас може бути більше двох диктів, і в цьому випадку ви можете використовувати:

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

де все буде додано до dict1.

[Примітка - я відредагував свою первинну відповідь, щоб мутувати перший аргумент; що полегшує пояснення "зменшити"]

ps в python 3, вам також знадобиться from functools import reduce


1
Потім ви можете вставити це всередині reduceабо еквівалентного циклу, щоб працювати з довільним числом dicts замість двох. Тим не менш, я не впевнений, що він робить те, що він хоче (або йому не було зрозуміло). Ви закінчите 2: {'c': {'z': {'y': {'info1': 'value', 'info2': 'value2'}}}, 'b': {'info1': 'value', 'info2': 'value2'}}свій другий приклад, я не впевнений, хоче він, zі він yзгладжений чи ні?
agf

1
це структури каталогів, тому я не думаю, що він / він хоче чогось вирівняного? о, вибачте, пропустили "кілька словників". так, зменшення було б добре. додасть це.
andrew cooke

Це робить саме те, що я хотів! Вибачте, що я був недостатньо зрозумілий ... Я думав, що з Python все в порядку, здається, ні: - / мені потрібна рекурсивна функція через вкладені дикти, ця працює, і я можу це зрозуміти :) я не Здається, зможе змусити його працювати зі зменшенням хоча ...
fdhex

2
Для тих , хто зі списками в якості остаточного рівня вкладеності під dicts, ви можете зробити це замість того, щоб піднімати помилку конкатеніровать два списки: a[key] = a[key] + b[key]. Дякуємо за корисну відповідь.
kevinmicke

1
> якщо ви хочете зберегти a, ви можете назвати його як злиття (dict (a), b). Зверніть увагу, що вкладені дикти все ще будуть мутовані. Щоб цього уникнути, використовуйте copy.deepcopy.
rcorre

30

Ось простий спосіб зробити це за допомогою генераторів:

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

Це відбитки:

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

якщо ви хочете зберегти тему генератора, ви могли б ланцюг (dict1.keys (), dict2.keys ())
andrew cooke

Хіба це не отримало б повторювані ключі?
jterrace

Цей, здається, спрацює, принаймні, на моєму наборі даних, але оскільки я ніколи не розумів врожайність та генератори, я дуже сильно втратив, чому, але я постараюся трохи важче, може бути корисним!
fdhex

ах, так, це отримало б повторювані ключі. вам все одно доведеться загорнути його в набір, вибачте.
andrew cooke

2
Я знайшов це спеціально корисним. Але найприємніше було б дозволити функції вирішувати конфлікти як параметр.
mentatkgs

25

Одне питання з цим питанням полягає в тому, що значення дикту можуть бути довільно складними фрагментами даних. На основі цих та інших відповідей я придумав цей код:

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

Мій випадок використання - це об'єднання файлів YAML, де я маю мати справу лише з підмножиною можливих типів даних. Тому я можу ігнорувати кортежі та інші предмети. Для мене розумна логіка злиття означає

  • замінити скалярів
  • додавати списки
  • злиття диктовок додаванням відсутніх ключів та оновленням існуючих ключів

Все інше і непередбачене призводить до помилки.


1
Фантастичний. Також добре працює на json звалищах. Щойно видалено помилку обробки. (Будучи ледачим, я можу зробити належні для json, я впевнений)
dgBP

3
послідовність "речовини" може бути замінена з isinstance(a, (str, unicode, int, long, float))нею?
simahawk

12

Словники словників зливаються

Оскільки це канонічне питання (незважаючи на певні загальні положення), я пропоную канонічний пітонічний підхід до вирішення цього питання.

Найпростіший випадок: "листя є вкладеними диктами, які закінчуються порожніми диктами":

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

Це найпростіший випадок рекурсії, і я рекомендував би два наївні підходи:

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

Я вважаю, що я віддав би перевагу другому, ніж першому, але майте на увазі, що початковий стан першого мав би бути перебудований з його походження. Ось використання:

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

Складний випадок: "листя будь-якого іншого типу:"

Отже, якщо вони закінчуються диктами, це простий випадок злиття кінцевих порожніх диктів. Якщо ні, то це не так банально. Якщо рядки, як їх зливати? Набори можуть бути оновлені аналогічно, тому ми могли б провести таке лікування, але ми втрачаємо порядок, в якому вони були об'єднані. Так чи має значення порядок?

Тож замість додаткової інформації найпростішим підходом буде надати їм стандартну обновлювальну обробку, якщо обидва значення не диктують: тобто значення другого дикта буде замінено першим, навіть якщо значення другого диктату є "Ні", а значення першого - диктант з великою кількістю інформації.

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

А зараз

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

повертає

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

Заява до оригінального питання:

Мені довелося зняти фігурні дужки навколо букв і поставити їх в єдині лапки, щоб це був легітимний Python (інакше вони будуть встановлені буквами в Python 2.7+), а також додати відсутніх дужки:

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

і rec_merge(dict1, dict2)тепер повертає:

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

Що відповідає бажаному результату вихідного питання (після зміни, наприклад, {A}до 'A'.)


10

За матеріалами @andrew cooke. Ця версія обробляє вкладені списки диктів, а також дозволяє опцію оновлення значень

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
Дякую, це так корисно. У мене в структурах постійно є списки диктів, інші рішення не можуть це належним чином злити.
Шернандес

7

Ця проста рекурсивна процедура об'єднає один словник в інший, змінюючи конфліктуючі ключі:

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

Вихід:

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

7

На основі відповідей від @andrew cooke. Він краще піклується про вкладені списки.

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]

інтуїтивно зрозумілий та симетричний. +1 для обробки списку :)
vdwees

6

Якщо у вас є невідомий рівень словників, то я б запропонував рекурсивну функцію:

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

Огляд

Наступний підхід розділяє проблему глибокого злиття диктів у:

  1. Параметризована функція неглибокого злиття, merge(f)(a,b)яка використовує функцію fдля об'єднання двох диктів aіb

  2. Рекурсивна функція злиття, fяку слід використовувати разом зmerge


Впровадження

Функцію для об'єднання двох (не вкладених) диктів можна записати безліччю способів. Мені особисто подобається

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

Хороший спосіб визначення відповідної функції рекурсивного злиття f- це використання кратного диспетчеризації, що дозволяє визначати функції, які оцінюються за різними шляхами, залежно від типу їх аргументів.

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)

Приклад

Для об'єднання двох вкладених диктів просто використовуйте, merge(f)наприклад:

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

Примітки:

Перевагами такого підходу є:

  • Функція побудована з менших функцій, кожна з яких робить одну річ, що робить код простішим для міркування та тестування

  • Поведінка не є жорстким кодом, але її можна змінювати та розширювати за потреби, що покращує повторне використання коду (див. Приклад нижче).


Настроювання

Деякі відповіді також розглядають дикти, які містять списки, наприклад, інші (потенційно вкладені) дикти. У цьому випадку можна захотіти зіставити списки та об'єднати їх залежно від позиції. Це можна зробити, додавши ще одне визначення функції злиття f:

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

4

Якщо хтось хоче ще одного підходу до цієї проблеми, ось моє рішення.

Чесноти : короткі, декларативні та функціональні за стилем (рекурсивні, не мутують).

Потенційний відкат : це може бути не злиття, яке ви шукаєте. Зверніться до доктрину щодо семантики.

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
        }

Дуже цікава відповідь, дякую, що поділилися нею. Який синтаксис ви використовували після оператора return? Я з цим не знайомий.
dev_does_software

4

Ви можете спробувати mergedeep .


Установка

$ pip3 install mergedeep

Використання

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

Щоб отримати повний список варіантів, ознайомтеся з документами !


3

Існує невелика проблема з відповіддю andrew Cookes: У деяких випадках він змінює другий аргумент, bколи ви змінюєте повернений дікт. Зокрема, це пов'язано з цим рядком:

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

Якщо b[key]це a dict, він буде просто призначений a, тобто будь-які наступні зміни, які dictвпливатимуть і на, aі на 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)

Щоб виправити це, рядок потрібно було б замінити таким:

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

Де 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

Все-таки. Це , очевидно , не враховує list, setі інші речі, але я сподіваюся , що це ілюструє підводні камені при спробі злиття dicts.

І задля повноти, ось моя версія, де ви можете передати її декілька 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, {})

Чому б не deepcopyзамість цього clone_dict?
Армандо Перес Маркес,

1
Тому що python stdlib є friggin величезним та чудовим! Я не мав поняття, що це існувало - плюс це було цікаво, що
кодувати

2

Ця версія функції буде враховувати N кількість словників, і лише словники - жодні неналежні параметри не можуть бути передані, або це підвищить TypeError. Саме злиття пояснює ключові конфлікти, і замість того, щоб перезаписувати дані зі словника далі по ланцюгу злиття, воно створює набір значень і додає до цього; дані не втрачаються.

Це може бути не найефективнішим на сторінці, але це найбільш ретельно, і ви не збираєтеся втрачати будь-яку інформацію, коли ви з’єднаєте свої 2 до 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})

вихід: {1: [1, 2], 2: {1: 2, 3: 1}, 4: 4}


2

Оскільки dictviews підтримують операції з набору, я зміг значно спростити відповідь 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])))

Будь-яка спроба поєднати dict з non dict (технічно об'єкт із методом 'keys' та об'єкт без методу 'keys') призведе до виникнення AttributeError. Це включає як початковий виклик функції, так і рекурсивні дзвінки. Це саме те, що я хотів, і я залишив це. Ви можете легко зловити AttributeErrors, кинутий рекурсивним викликом, а потім отримати будь-яке значення, яке вам подобається.


2

Короткий п-солодкий:

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]

Це працює на зразок dict.updateметоду Пітона . Він повертається None(ви завжди можете додати, return dякщо хочете), оскільки він оновлює dict dна місці. Клавіші in vперезаписують будь-які наявні ключі d(він не намагається інтерпретувати вміст дикту).

Він також буде працювати для інших ("диктоподібних") відображень.


1

Код, звичайно, залежатиме від ваших правил вирішення конфліктів злиття. Ось версія, яка може приймати довільну кількість аргументів і об'єднувати їх рекурсивно до довільної глибини, не використовуючи жодної мутації об'єкта. Для вирішення конфліктів об'єднання використовуються такі правила:

  • словники мають перевагу над недиктичними значеннями ( {"foo": {...}}має перевагу над {"foo": "bar"})
  • пізніші аргументи мають пріоритет по порівнянні з попередніми аргументами (якщо ви зливаєте {"a": 1}, {"a", 2}і {"a": 3}в порядку, то результат буде {"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

У мене було два словники ( aі b), які могли б містити будь-яку кількість вкладених словників. Я хотів рекурсивно їх об'єднати, bмаючи перевагу над a.

Розглядаючи вкладені словники як дерева, я хотів:

  • Оновити aтак, щоб кожен шлях до кожного аркуша в bбув представлений уa
  • Перезаписати підрядки, aякщо аркуш знайдено у відповідному шляху вb
    • Дотримуйтесь інваріантності того, що всі bлистяні вузли залишаються листками.

Існуючі відповіді були трохи складні на мій смак і залишили деякі подробиці на полиці. Я зламав наступне, яке проходить одиничні тести для мого набору даних.

  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

Приклад (відформатований для ясності):

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

Шляхи, bякі потрібно було підтримувати:

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

a мали унікальні та безперебійні шляхи:

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

тому вони все ще представлені на об’єднаній карті.


1

У мене є ітеративне рішення - працює набагато краще з великими диктами & багато з них (наприклад, jsons тощо):

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

зауважте, що це використовуватиме значення d2 для переосмислення d1, якщо вони не є обома диктами. (те саме, що і пітона dict.update())

кілька тестів:

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

Я тестував приблизно ~ 1200 диктів - цей метод займав 0,4 секунди, тоді як рекурсивний розчин займав ~ 2,5 секунди.


0

Це повинно допомогти в об'єднанні всіх елементів з dict2в 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]

Будь ласка, протестуйте його та скажіть, чи це ви хотіли.

Редагувати:

Вищезазначене рішення об'єднує лише один рівень, але правильно вирішує приклад, поданий ОП. Для злиття декількох рівнів слід використовувати рекурсію.


1
У нього довільна глибина гніздування
1111 р.

Це можна переписати просто так for k,v in dict2.iteritems(): dict1.setdefault(k,{}).update(v). Але як вказував @agf, це не зливає вкладені дикти.
Шон Чін

@agf: Правильно, тому, здається, ОП потребує рішення із застосуванням рецидивів. Завдяки тому, що словники є змінними, це зробити досить просто. Але я думаю, що питання недостатньо конкретне, щоб сказати, що повинно статися, коли ми знаходимо місця з різним рівнем глибини (наприклад, намагаючись злитися {'a':'b'}з {'a':{'c':'d'}).
Тадек

0

Я тестував ваші рішення та вирішив використати це у своєму проекті:

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

Передача функцій у якості параметрів є ключовим для розширення рішення jterrace таким чином, щоб діяти як усі інші рекурсивні рішення.


0

Найпростіший спосіб, що я можу придумати, це:

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

Вихід:

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

0

У мене є ще одне трохи інше рішення:

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

За замовчуванням він вирішує конфлікти на користь значень другого диктату, але ви можете легко перекрити це, з деяким чаклунством ви зможете навіть викинути з нього винятки. :).


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

ей там у мене також була така ж проблема, але я хоч і вирішив, і я опублікую її тут, якщо це також буде корисно для інших, в основному злиття вкладених словників, а також додавання значень, для мене мені потрібно було обчислити деякі ймовірності, щоб це один працював чудово:

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

За допомогою описаного вище способу ми можемо об'єднати:

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

і це стане: {'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 }}

також помічайте зміни тут:

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

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

не забудьте також додати імпорт для копії:

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

Вихід:

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

Хоча цей код може відповісти на питання, надаючи додатковий контекст стосовно того, чому та / або як цей код відповідає на питання, покращує його довгострокове значення.
xiawi

Я думаю, що це загальна реалізація об'єднання одного або декількох вкладених словників з урахуванням типу об'єктів, які будуть
маркіровані

0

подивіться на toolzпакет

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

дає

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

0

Наступна функція об'єднує b в 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

І ще одна незначна зміна:

Ось чистий набір python3 на основі функції глибокого оновлення. Він оновлює вкладені словники, перебираючи по одному рівні за один раз і закликає себе оновлювати кожен наступний рівень значень словника:

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

Простий приклад:

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

Як щодо іншої відповіді?!? Цей також уникає мутації / побічних ефектів:

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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.