在Python中深度合并字典的字典

215
我需要合并多个字典,这是我目前的例子:
dict1 = {1:{"a":{"A"}}, 2:{"b":{"B"}}}

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

A B CD 是树的叶子节点,就像 {"info1":"value", "info2":"value2"} 这样。

有一个未知层级(深度)的字典,可能是 {2:{"c":{"z":{"y":{C}}}}}

在我的情况下,它代表一个目录/文件结构,其中节点是文档,叶子是文件。

我想要合并它们以获得:

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

我不确定如何用Python轻松地做到这一点。

请查看我的NestedDict类:http://stackoverflow.com/a/16296144/2334951 它可以管理嵌套字典结构,如合并等操作。 - SzieberthAdam
3
提醒所有寻找解决方案的人:本问题仅涉及嵌套字典。大多数答案无法正确处理结构中包含字典列表的更复杂情况。如果您需要此功能,请尝试@Osiloke的答案:https://dev59.com/IWw05IYBdhLWcg3wfx80#25270947 - SHernandez
参见:python dpath merge - dreftymac
@andrew cooke的解决方案的一个陷阱是,即使存在冲突错误,更改也会影响第一个字典。为了避免这个陷阱,可以使用@andrew cooke的源代码创建一个递归辅助函数,并添加一个参数,该参数具有第一个字典的克隆。该参数将被更改并返回,而不是第一个字典。请参见:https://dev59.com/IWw05IYBdhLWcg3wfx80#71700270 - diogo
可以使用Addict来合并字典:d = Dict({1:{"a":{'A'}}, 2:{"b":{'B'}}}); d.update({2:{"c":{'C'}}, 3:{"d":{'D'}}}); d => {1: {'a': {'A'}}, 2: {'b': {'B'}, 'c': {'C'}}, 3: {'d': {'D'}}} - bartolo-otrit
显示剩余2条评论
36个回答

205

实际上这相当棘手 - 特别是如果你想要一个有用的错误消息来处理不一致的情况,同时正确接受重复但一致的条目(这是其他答案没有做到的)。

假设你的条目数量不是很大,使用递归函数是最简单的方法:

from functools import reduce

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)。如果您想保留a,可以像这样调用:merge(dict(a), b)

正如agf(下面)指出的那样,您可能有多个字典,此时您可以使用:

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

所有内容都将被添加到dict1中。

注意:我编辑了我的初始答案以改变第一个参数;这使得“reduce”更容易解释。


1
然后,您可以将其放入“reduce”或等效循环中,以处理任意数量的“dict”,而不是两个。但是,我不确定这是否符合他的要求(他没有明确说明)。对于他的第二个示例,您最终会得到2:{'c':{'z':{'y':{'info1':'value','info2':'value2'}}},'b':{'info1':'value','info2':'value2'}},我不确定他是否希望将“z”和“y”展开? - agf
1
它们是目录结构,所以我认为他/她不想要任何被压平的东西?哦,抱歉,我错过了“多个字典”。是的,使用reduce会很好。我会添加的。 - andrew cooke
2
对于那些在字典的最终嵌套级别下具有列表的人,您可以执行以下操作,而不是引发错误以连接两个列表:a[key] = a[key] + b[key]。感谢您提供的有用答案。 - kevinmicke
@andrew cooke:如果这个合并操作需要在大量记录上执行,比如数百万条记录,那么应该采取什么方法?不适合使用递归吗? - Thirsty_Crow
3
如果你想保留a,你可以像这样调用merge(dict(a), b)。请注意,嵌套的字典仍将被改变。为避免这种情况,请使用copy.deepcopy - rcorre
显示剩余10条评论

58

你可以尝试使用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}}

要查看完整选项列表,请查看文档


如果我执行merge(a, b),那么a也会被更新。为什么会发生这种情况? - vasili111
2
我找到了解决方案,它需要使用merge({}, a, b)。 - vasili111
@vasili111 因为 merge(a,b) 会将 b 原地合并到 a 中。 - Cornelius Roemer
这个图书馆看起来没有维护了。:( - Liz Av

50

以下是使用生成器的简单方法:

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

这个似乎可以完成工作,至少在我的数据集上是这样,但由于我从未真正理解yield和生成器,所以我对为什么能够完成工作感到困惑,但我会更加努力地尝试,这可能会很有用! - fdhex
2
我发现这特别有帮助。但最好的方式是将解决冲突的函数作为参数传递。 - mentatkgs
我认为如果两个字典具有相同的键,而其中一个对应的值是字典,而另一个不是,则它将无法工作。例如:dict1 = {1:{"a":"A"},2:{"b":"B"}} dict2 = {2:"c",3:{"d":"D"}} - kispaljr
1
非常感谢,这个解决方案在使用大型字典时非常有效(@andrew cook的解决方案也不错,但需要将所有字典加载到内存中)。@kispaljr确实,这是针对具有相同格式的字典而设计的。 - Floran Gmehlin
3
为了增加代码的优雅性,可以去掉yield函数中的括号,并将第一个for循环改写成 for k in set(dict1) | set(dict2): - Matthew D. Scholefield
显示剩余3条评论

29

这个问题的一个难点在于字典的值可以是任意复杂的数据。根据这些答案和其他信息,我设计出了以下代码:

一个问题是该问题中字典的值可以是任意复杂数据,我结合其他回答编写了以下代码:

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
8
“isinstance”序列可以被替换为isinstance(a, (str, unicode, int, long, float)),对吗? - simahawk
在这里找到了使用SequenceMutableMappingMapping进行更新的案例链接 - Viet Than

26

字典的字典合并

尽管存在某些非通用情况,但这是规范问题,我提供了解决此问题的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.abc 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():
        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')。


17

根据 @andrew cooke 的版本,此版本处理了嵌套字典列表,并允许更新值的选项。

def merge(a, b, path=None, update=True):
    "https://dev59.com/IWw05IYBdhLWcg3wfx80
    "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
谢谢,这非常有帮助。我一直在我的结构中使用字典列表,其他解决方案无法正确合并它们。 - SHernandez
1
假设您的字典列表长度相同。如果您有不同长度的列表,这将失败。 - Brendan

11

这个简单的递归过程将一个字典合并到另一个字典中,同时覆盖冲突的键:

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

8

根据@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
谢谢您提供的好例子 :)。经过一些修改后,它对我有用了! - LukasS

7

如果有人希望尝试另一种解决这个问题的方法,这是我的解决方案。

优点:简短,声明式和函数式风格(递归,不进行突变)。

潜在缺点:这可能不是您要查找的合并结果。请参阅文档字符串以获取语义信息。

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语句之后,您使用了什么语法?我不熟悉它。 - schmittsfn
2
@schmittsfn 这是一个字典推导式。翻译成中文为dict([key, deep_merge(a.get(key), b.get(key))] for key in keys) - user430953

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接