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

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

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 ]

2 голосов
/ 13 апреля 2018

Обзор

Следующий подход подразделяет проблему глубокого слияния диктов на:

  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 использует multipledispatch , который позволяет определять функции, которые оценивают по разным путям в зависимости от типа их аргументов.

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)

* Пример 1 040 *

Чтобы объединить два вложенных дикта, просто используйте merge(f) например ::1010*

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,fillvalue={})]
2 голосов
/ 21 декабря 2014

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

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

Если b[key] является 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, {})
1 голос
/ 26 августа 2011

Это должно помочь объединить все элементы из 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]

Пожалуйста, проверьте это и скажите нам, действительно ли это то, что вы хотели.

EDIT:

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

1 голос
/ 11 мая 2016

У меня было два словаря (a и b), каждый из которых может содержать любое количество вложенных словарей. Я хотел их рекурсивно объединить, и b будет иметь приоритет над a.

Рассматривая вложенные словари как деревья, я хотел:

  • Чтобы обновить a, чтобы каждый путь к каждому листу в b был представлен в a
  • Перезаписать поддеревья a, если в соответствующем пути в * 1016 найден лист
    • Поддерживать инвариант, что все 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 голос
/ 13 сентября 2018

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]

Это работает как (и основывается) на методе Python dict.update. Он возвращает None (вы всегда можете добавить return d, если хотите), так как он обновляет dict d на месте. Ключи в v будут перезаписывать любые существующие ключи в d (он не пытается интерпретировать содержимое dict).

Это также будет работать для других ("похожих на dict") отображений.

0 голосов
/ 17 мая 2019

Если кто-то еще хочет другой подход к этой проблеме, вот мое решение.

Добродетели : короткие, декларативные и функциональные по стилю (рекурсивные, без мутаций).

Потенциальный недостаток : Возможно, это не то слияние, которое вы ищете. Консультируйтесь со строкой документации для семантики.

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
        }
0 голосов
/ 20 октября 2018

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

#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 голосов
/ 10 января 2012

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

  • словари имеют приоритет над недиктивными значениями ({"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  
0 голосов
/ 24 мая 2012

Я тестировал ваши решения и решил использовать это в своем проекте:

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 голосов
/ 12 апреля 2018
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()
...