Словари слияния словарей - PullRequest
       66

Словари слияния словарей

98 голосов
/ 26 августа 2011

Мне нужно объединить несколько словарей, вот что у меня есть, например:

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.

Ответы [ 22 ]

114 голосов
/ 26 августа 2011

это на самом деле довольно сложно - особенно если вы хотите получить полезное сообщение об ошибке, когда что-то противоречиво, и при этом правильно принимает дубликаты, но согласованные записи (что-то, чего другой ответ здесь не делает ...)

при условии, что вы не понимаетене имеет большого количества записей, рекурсивная функция проще всего:

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.

[примечание - я отредактировал свой первоначальный ответ, чтобы изменить первый аргумент;это облегчает объяснение «снижения»]

пс в Python 3, вам также понадобится from functools import reduce

24 голосов
/ 26 августа 2011

Вот простой способ сделать это, используя генераторы:

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'}}
18 голосов
/ 05 апреля 2013

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

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 , где мне нужно иметь дело только с подмножеством возможных типов данных.Следовательно, я могу игнорировать кортежи и другие объекты.Для меня разумная логика слияния означает

  • заменить скаляры
  • добавить списки
  • диктовать слияния путем добавления отсутствующих ключей и обновления существующих ключей

Все остальное и непредвиденное приводят к ошибке.

10 голосов
/ 06 июня 2014

Словари слияния словарей

Поскольку это канонический вопрос (несмотря на некоторые необщности), я даю канонический подход Pythonic к решению этой проблемы.

Простейший случай: «листья - это вложенные дикты, заканчивающиеся пустыми»:

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

Сложный кейс: «листья любого другого типа:»

Так что, если они заканчиваются диктатами, это простой случай слияния конечных пустых диктов. Если нет, то это не так тривиально. Если строки, как вы их объединить? Наборы можно обновлять аналогичным образом, чтобы мы могли выполнить эту обработку, но мы теряем порядок, в котором они были объединены. Так имеет ли значение порядок?

Таким образом, вместо получения дополнительной информации, простейший подход состоит в том, чтобы дать им стандартную обработку обновления, если оба значения не являются диктатами: т.е. значение второго диктанта будет перезаписывать первое, даже если значение второго диктанта равно None, а значение первого значение - это диктат с большим количеством информации.

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

8 голосов
/ 12 августа 2014

Основано на @andrew cooke.Эта версия обрабатывает вложенные списки диктов, а также позволяет обновлять значения

def merge(a, b, path=None, update=True):
    "/4298591/slovari-sliyaniya-slovarei"
    "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
6 голосов
/ 26 августа 2011

Если у вас есть неизвестный уровень словарей, то я бы предложил рекурсивную функцию:

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
4 голосов
/ 09 июня 2018

Основано на ответах @andrew cooke.Он лучше обрабатывает вложенные списки.

def deep_merge_lists(original, incoming):
    """
    Deep merge two lists. Modifies original.
    Reursively 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: Calls 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:
            orginal[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. Modfies original.
    For key conflicts if both values are:
     a. dict: Recursivley call deep_merge_dicts on both values.
     b. list: Calls 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]
3 голосов
/ 19 июля 2014

Эта простая рекурсивная процедура объединит один словарь в другой, перекрывая конфликтующие ключи:

#!/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'}}
2 голосов
/ 10 февраля 2016

Поскольку 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 голосов
/ 25 августа 2013

Эта версия функции будет учитывать N количество словарей, и только словари - никакие неправильные параметры не могут быть переданы, иначе возникнет ошибка TypeError.Само слияние учитывает конфликты ключей, и вместо того, чтобы перезаписывать данные из словаря далее по цепочке слияния, оно создает набор значений и дополняет их;данные не будут потеряны.

Возможно, это не самая эффективная информация на странице, но она самая тщательная, и вы не потеряете какую-либо информацию при слиянии двух до N диктов.1004 *

вывод: {1: [1, 2], 2: {1: 2, 3: 1}, 4: 4}

...