Как слить словари словарей?


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
Предупреждение всем, кто ищет решения: этот вопрос касается только вложенных диктовок. Большинство ответов не обрабатывают более сложный случай списков dicts внутри структуры должным образом. Если вам это нужно, попробуйте ответ @Osiloke ниже: stackoverflow.com/a/25270947/1431660
Шернандес

См. Также: 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 указал (ниже), что у вас может быть более двух dicts, и в этом случае вы можете использовать:

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
это структуры каталогов, поэтому я не думаю, что он / она хочет что-то сглаженное? ой, извините, пропустил "несколько словарей". да, уменьшить было бы хорошо. добавлю это.
Эндрю Кук

Это именно то, что я хотел! Мне жаль, что я недостаточно ясно понял ... Я думал, что с Python все в порядке, похоже, нет: - / Мне нужна была рекурсивная функция из-за вложенных dicts, она работает, и я могу ее понять :) Я не похоже, может заставить его работать с помощью reduce ...
fdhex

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

1
> если вы хотите сохранить, вы можете назвать это как merge (dict (a), b). Обратите внимание, что вложенные dicts все равно будут видоизменяться. Чтобы этого избежать, используйте copy.deepcopy.
rcorre

31

Вот простой способ сделать это с помощью генераторов:

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 ())
Эндрю Кук

Разве это не приведет к дублированию ключей?
jterrace

Кажется, это работает, по крайней мере, с моим набором данных, но, поскольку я никогда не понимал yield и генераторы, я почти не понимаю, почему, но я постараюсь немного больше, может быть полезно!
fdhex

ах, да, он получит дубликаты ключей. вам все равно нужно будет завернуть его в набор, извините.
Эндрю Кук

2
Я нашел это особенно полезным. Но лучше всего было бы позволить функции разрешать конфликты в качестве параметра.
mentatkgs

25

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

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

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


1
Фантастика. Хорошо работает и с json-дампами. Просто убрал обработку ошибок. (Будучи ленивым, я уверен, может сделать правильные для json)
dgBP

3
последовательность "isinstance" можно заменить без 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': {}}}}

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

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

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

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. Эта версия обрабатывает вложенные списки dicts, а также позволяет обновлять значения

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
Спасибо, это так полезно. У меня все время есть списки dicts в моих структурах, другие решения не могут правильно объединить это.
Шернандес 08

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для слияния двух dicts aиb

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


Реализация

Функцию слияния двух (не вложенных) dicts можно написать разными способами. Лично мне нравится

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

Есть небольшая проблема с ответом Эндрю Кука: в некоторых случаях он изменяет второй аргумент, bкогда вы изменяете возвращаемый dict. В частности, из-за этой строки:

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
Потому что stdlib python чертовски огромен и великолепен! Я понятия не имел, что это существует - к тому же кодировать это было забавной
мелочью

2

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

Возможно, он не самый эффективный на странице, но он самый тщательный, и вы не потеряете никакой информации при объединении двух слов в 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 (технически, объект с методом «ключей» и объект без метода «ключей») вызовет ошибку AttributeError. Это включает как начальный вызов функции, так и рекурсивные вызовы. Это именно то, что я хотел, поэтому я оставил это. Вы можете легко поймать AttributeErrors, вызванные рекурсивным вызовом, и затем выдать любое значение, которое вам нравится.


2

Short-н-сладкий:

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метод Python (и основан на нем) . Он возвращается None(вы всегда можете добавить, return dесли хотите), поскольку он обновляет dict dна месте. Ключи в vбудут перезаписывать любые существующие ключи в d(он не пытается интерпретировать содержимое dict).

Это также будет работать для других («dict-like») сопоставлений.


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

У меня есть итеративное решение - намного лучше работает с большими dicts и многими из них (например, 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, если они не являются обоими dicts. (как у питона 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]

Пожалуйста, протестируйте его и сообщите нам, что вы хотели.

РЕДАКТИРОВАТЬ:

Вышеупомянутое решение объединяет только один уровень, но правильно решает пример, данный OP. Для объединения нескольких уровней следует использовать рекурсию.


1
У него произвольная глубина вложенности
agf

Это можно просто переписать как for k,v in dict2.iteritems(): dict1.setdefault(k,{}).update(v). Но, как указал @agf, это не объединяет вложенные dicts.
Шон Чин

@agf: Правильно, поэтому кажется, что OP требует решения, использующего повторение. Благодаря тому, что словари изменяемы, это должно быть довольно легко сделать. Но я думаю, что вопрос недостаточно конкретен, чтобы сказать, что должно произойти, когда мы придумываем места с разным уровнем глубины (например, пытаемся слиться {'a':'b'}с {'a':{'c':'d'}).
Tadeck

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.