Словари слияния словарей
Мне нужно объединить несколько словарей, вот что у меня есть, например:
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.
35 ответов
На самом деле это довольно сложно - особенно если вы хотите получить полезное сообщение об ошибке, когда что-то противоречиво, и при этом правильно принимает повторяющиеся, но непротиворечивые записи (что-то, чего другой ответ здесь не делает...)
при условии, что у вас нет большого количества записей, рекурсивная функция проще всего:
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
Вы можете попробовать 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}}
Полный список опций можно найти в документации!
Вот простой способ сделать это с помощью генераторов:
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'}}
Одна проблема с этим вопросом состоит в том, что значения 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, когда мне нужно иметь дело только с подмножеством возможных типов данных. Следовательно, я могу игнорировать кортежи и другие объекты. Для меня разумная логика означает слияние
- заменить скаляры
- добавить списки
- объединение диктов путем добавления отсутствующих ключей и обновления существующих ключей
Все остальное и непредвиденное приводит к ошибке.
Словари слияния словарей
Поскольку это канонический вопрос (несмотря на некоторые необщности), я предоставляю канонический подход 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'
.)
Основано на @andrew cooke. Эта версия обрабатывает вложенные списки диктов, а также позволяет обновлять значения
def merge(a, b, path=None, update=True): "Http://stackru.com/questions/7204805/python-dictionaries-of-dictionaries-merge" "сливает б в" если путь отсутствует: путь = [] для ключа в б: если введите: если isinstance (a [ключ], dict) и isinstance (b [ключ], dict): объединить (a [ключ], b [ключ], путь + [str(ключ)]) elif a [ключ] == b [ключ]: передать # то же значение листа elif isinstance (a [ключ], список) и isinstance (b [ключ], список): для idx, val в перечислении (b [ключ]): a [ключ][idx] = слияние (a [ключ][idx], b[ключ] [idx], путь + [str(ключ), str(idx)], обновление = обновление) обновление elif: a [ключ] = b [ключ] еще: повысить исключение ('Конфликт в%s' % '.'.join(путь + [str(ключ)])) еще: a [ключ] = b [ключ] вернуть
Эта простая рекурсивная процедура объединит один словарь в другой, переопределяя конфликтующие ключи:
#!/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'}}
В случае, если кто-то хочет еще один подход к этой проблеме, вот мое решение.
Добродетели: короткие, декларативные и функциональные по стилю (рекурсивные, без мутаций).
Потенциальный недостаток: это может быть не то слияние, которое вы ищете. Консультируйтесь со строкой документации для семантики.
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
}
Основано на ответах @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]
Short-н-сладкий:
from collections 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
если ты предпочитаешь) как он обновляет дикт d
на месте. Ключи в v
перезапишет любые существующие ключи в d
(он не пытается интерпретировать содержание диктата).
Это также будет работать для других ("dict-like") отображений.
Если у вас есть неизвестный уровень словарей, то я бы предложил рекурсивную функцию:
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
обзор
Следующий подход подразделяет проблему глубокого слияния диктов на:
Параметризованная функция мелкого слияния
merge(f)(a,b)
который использует функциюf
объединить два диктатаa
а такжеb
Рекурсивная функция слияния
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)
пример
Для объединения двух вложенных диктов просто используйте 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,fillvalue={})]
У меня есть итеративное решение - намного лучше работает с большими 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. (то же, что и у pythondict.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 секунды.
Как отмечалось во многих других ответах, здесь наиболее целесообразен рекурсивный алгоритм. В общем, при работе с рекурсией предпочтительнее создавать новые значения, а не пытаться модифицировать какую-либо структуру входных данных.
Нам нужно определить, что происходит на каждом этапе слияния. Если оба входа являются словарями, это легко: мы копируем уникальные ключи с каждой стороны и рекурсивно объединяем значения дублированных ключей. Это базовые случаи, которые вызывают проблему. Будет проще понять логику, если мы вытащим для этого отдельную функцию. В качестве заполнителя мы могли бы просто обернуть два значения в кортеж:
def merge_leaves(x, y):
return (x, y)
Теперь ядро нашей логики выглядит так:
def merge(x, y):
if not(isinstance(x, dict) and isinstance(y, dict)):
return merge_leaves(x, y)
x_keys, y_keys = x.keys(), y.keys()
result = { k: merge(x[k], y[k]) for k in x_keys & y_keys }
result.update({k: x[k] for k in x_keys - y_keys})
result.update({k: y[k] for k in y_keys - x_keys})
return result
Давайте проверим это:
>>> x = {'a': {'b': 'c', 'd': 'e'}, 'f': 1, 'g': {'h', 'i'}, 'j': None}
>>> y = {'a': {'d': 'e', 'h': 'i'}, 'f': {'b': 'c'}, 'g': 1, 'k': None}
>>> merge(x, y)
{'f': (1, {'b': 'c'}), 'g': ({'h', 'i'}, 1), 'a': {'d': ('e', 'e'), 'b': 'c', 'h': 'i'}, 'j': None, 'k': None}
>>> x # The originals are unmodified.
{'a': {'b': 'c', 'd': 'e'}, 'f': 1, 'g': {'h', 'i'}, 'j': None}
>>> y
{'a': {'d': 'e', 'h': 'i'}, 'f': {'b': 'c'}, 'g': 1, 'k': None}
Мы можем легко изменить правило слияния листьев, например:
def merge_leaves(x, y):
try:
return x + y
except TypeError:
return Ellipsis
и наблюдайте за эффектами:
>>> merge(x, y)
{'f': Ellipsis, 'g': Ellipsis, 'a': {'d': 'ee', 'b': 'c', 'h': 'i'}, 'j': None, 'k': None}
Мы также потенциально могли бы исправить это, используя стороннюю библиотеку для диспетчеризации в зависимости от типа входных данных. Например, используя multidispatch, мы могли бы делать такие вещи, как:
@dispatch(dict, dict)
def merge(x, y):
x_keys, y_keys = x.keys(), y.keys()
result = { k: merge(x[k], y[k]) for k in x_keys & y_keys }
result.update({k: x[k] for k in x_keys - y_keys})
result.update({k: y[k] for k in y_keys - x_keys})
return result
@dispatch(str, str)
def merge(x, y):
return x + y
@dispatch(tuple, tuple)
def merge(x, y):
return x + y
@dispatch(list, list)
def merge(x, y):
return x + y
@dispatch(int, int):
def merge(x, y):
raise ValueError("integer value conflict")
@dispatch(object, object):
return (x, y)
Это позволяет нам обрабатывать различные комбинации особых случаев листового типа без написания собственной проверки типов, а также заменяет проверку типов в основной рекурсивной функции.
Есть небольшая проблема с ответом Эндрю Кука: в некоторых случаях он изменяет второй аргумент 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, {})
Взгляни на 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'}}
Поскольку 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, генерируемые рекурсивным вызовом, и затем получить любое значение, какое пожелаете.
Следующая функция объединяет 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
В этой версии функции будет учтено 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}
Как насчет другого ответа?!? Этот также позволяет избежать мутации / побочных эффектов:
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)
Конечно, код будет зависеть от ваших правил разрешения конфликтов слияния. Вот версия, которая может принимать произвольное количество аргументов и рекурсивно сливать их на произвольную глубину без использования каких-либо объектных мутаций. Для разрешения конфликтов слияния используются следующие правила:
- словари имеют приоритет над недиктивными значениями (
{"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
Это должно помочь в объединении всех элементов из 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. Чтобы объединить несколько уровней, следует использовать рекурсию.
У меня было два словаря (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'
поэтому они все еще представлены на объединенной карте.
И еще одна небольшая вариация:
Вот чистая функция глубокого обновления на основе набора 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}
Это решение, которое я сделал, рекурсивно объединяет словари до бесконечной глубины. Первый словарь, переданный в функцию, является главным словарем — значения в нем будут перезаписывать значения в том же ключе во втором словаре.
def merge(dict1: dict, dict2: dict) -> dict:
merged = dict1
for key in dict2:
if type(dict2[key]) == dict:
merged[key] = merge(dict1[key] if key in dict1 else {}, dict2[key])
else:
if key not in dict1.keys():
merged[key] = dict2[key]
return merged
Самый простой способ, который я могу придумать, это:
#!/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'}}
Возврат слияния без воздействия на входные словари.
def _merge_dicts(dictA: Dict = {}, dictB: Dict = {}) -> Dict:
# it suffices to pass as an argument a clone of `dictA`
return _merge_dicts_aux(dictA, dictB, copy(dictA))
def _merge_dicts_aux(dictA: Dict = {}, dictB: Dict = {}, result: Dict = {}, path: List[str] = None) -> Dict:
# conflict path, None if none
if path is None:
path = []
for key in dictB:
# if the key doesn't exist in A, add the B element to A
if key not in dictA:
result[key] = dictB[key]
else:
# if the key value is a dict, both in A and in B, merge the dicts
if isinstance(dictA[key], dict) and isinstance(dictB[key], dict):
_merge_dicts_aux(dictA[key], dictB[key], result[key], path + [str(key)])
# if the key value is the same in A and in B, ignore
elif dictA[key] == dictB[key]:
pass
# if the key value differs in A and in B, raise error
else:
err: str = f"Conflict at {'.'.join(path + [str(key)])}"
raise Exception(err)
return result
вдохновлен решением @andrew cooke
def m(a,b):
aa = {
k : dict(a.get(k,{}), **v) for k,v in b.items()
}
aap = print(aa)
return aap
d1 = {1:{"a":"A"}, 2:{"b":"B"}}
d2 = {2:{"c":"C"}, 3:{"d":"D"}}
dict1 = {1:{"a":{1}}, 2:{"b":{2}}}
dict2 = {2:{"c":{222}}, 3:{"d":{3}}}
m(d1,d2)
m(dict1,dict2)
"""
Output :
{2: {'b': 'B', 'c': 'C'}, 3: {'d': 'D'}}
{2: {'b': {2}, 'c': {222}}, 3: {'d': {3}}}
"""
Я не проверял это подробно, поэтому ваши отзывы приветствуются.
from collections import defaultdict
dict1 = defaultdict(list)
dict2= defaultdict(list)
dict3= defaultdict(list)
dict1= dict(zip(Keys[ ],values[ ]))
dict2 = dict(zip(Keys[ ],values[ ]))
def mergeDict(dict1, dict2):
dict3 = {**dict1, **dict2}
for key, value in dict3.items():
if key in dict1 and key in dict2:
dict3[key] = [value , dict1[key]]
return dict3
dict3 = mergeDict(dict1, dict2)
#sort keys alphabetically.
dict3.keys()
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}}