Python:如何递归合并两个字典?

3
假设我们有两个字典:
a = {
    "key1": "value1",
    "key2": "value2",
    "key3": {
        "key3_1": "value3_1",
        "key3_2": "value3_2"
    }
}

b = {
    "key1": "not_key1",
    "key4": "something new",
    "key3": {
        "key3_1": "Definitely not value3_1",
        "key": "new key without index?"
    }
}

由于合并的结果,我需要获取以下字典:
{
    "key1": "not_key1",
    "key2": "value2",
    "key3": {
        "key3_1": "Definitely not value3_1",
        "key3_2": "value3_2",
        "key": "new key without index?"
    },
    "key4": "something new"
}

I have this kind of code:

def merge_2_dicts(dict1, dict2):
    for i in dict2:
        if not type(dict2[i]) == dict:
            dict1[i] = dict2[i]
        else:
            print(dict1[i], dict2[i], sep="\n")
            dict1[i] = merge_2_dicts(dict1[i], dict2[i])
    return dict1

这个方法可以正常工作并给我想要的结果,但是我不确定是否有更简单的方式。是否有更简单/更短的选项?


你的解决方案看起来不错,最重要的是它能够工作。我的唯一担心是,如果dict1中有一个键不在dict2中怎么办?目前,你忽略了它。 - aaossa
@aaossa,嗯,我好像没有遇到过这个问题。 在这个例子中,只有a中有["key_3"]["key_3_2"],而b中没有。输出时只需要字典即可。 - user18196171
2
一种可能的改进是使用isinstance(X, dict)代替type(X) == dict。请参见此问题 - knia
@knia,谢谢,我完全忘记了。 - user18196171
1
你的需求是合并两个字典,但是字典的顺序似乎影响了你的结果。一般来说,合并两个字典不应该依赖于顺序。如果你能扩展你的代码以处理合并多个字典,那就更好了。除非你澄清顺序对预期结果的影响,否则我不会写任何代码。 - Lei Yang
显示剩余2条评论
4个回答

1

我认为你的代码几乎很好。我只看到一个问题,如果目标字典中缺少键怎么办?

def merge_dicts(tgt, enhancer):
    for key, val in enhancer.items():
        if key not in tgt:
            tgt[key] = val
            continue

        if isinstance(val, dict):
            merge_dicts(tgt[key], val)
        else:
            tgt[key] = val
    return tgt

这段代码与您所写的大部分内容相同。

  1. 检查目标字典中是否存在键,如果不存在则无论类型如何都进行更新。
  2. 如果值为字典,则使用递归。
  3. 如果值不是字典,则从增强字典中进行更新。

但是我仍然看到一个问题,如果目标字典中的值是字符串,而增强字典中的值是字典怎么办?

enhancer = {
    "key3": {
        "key3_1": "value3_1",
        "key3_2": "value3_2"
    }
}

tgt = {
    "key3": "string_val"
}

那就要看你喜欢哪种方式了:

  1. 使用增强器中的字典覆盖字符串:
def merge_dicts(tgt, enhancer):
    for key, val in enhancer.items():
        if key not in tgt:
            tgt[key] = val
            continue

        if isinstance(val, dict):
            if not isinstance(tgt[key], dict):
                tgt[key] = dict()
            merge_dicts(tgt[key], val)
        else:
            tgt[key] = val
    return tgt

保留目标字典中的字符串值:
def merge_dicts(tgt, enhancer):
    for key, val in enhancer.items():
        if key not in tgt:
            tgt[key] = val
            continue

        if isinstance(val, dict):
            if not isinstance(tgt[key], dict):
                continue
            merge_dicts(tgt[key], val)
        else:
            tgt[key] = val
    return tgt

0
另一种解决方案:
from copy import deepcopy
from typing import Any


def is_all_dict(a1: Any, a2: Any) -> bool:
    return isinstance(a1, dict) and isinstance(a2, dict)


def recursively_merge(d1: dict, d2: dict) -> dict:
    d = deepcopy(d1)
    for k, v2 in d2.items():
        if (v := d.get(k)) and is_all_dict(v, v2):
            sub_dicts = []
            for sk, sv2 in v2.items():
                if (sv := v.get(sk)) and is_all_dict(sv, sv2):
                    sub_dicts.append((sv, sv2))
                else:
                    v[sk] = sv2
            while sub_dicts:
                sds = []
                for v, v2 in sub_dicts:
                    for sk, sv2 in v2.items():
                        if (sv := v.get(sk)) and is_all_dict(sv, sv2):
                            sds.append((sv, sv2))
                        else:
                            v[sk] = sv2
                sub_dicts = sds
        else:
            d[k] = v2
    return d

输出:

In [26]: import pprint

In [27]: pprint.pprint(recursively_merge(a, b))
{'key1': 'not_key1',
 'key2': 'value2',
 'key3': {'key': 'new key without index?',
          'key3_1': 'Definitely not value3_1',
          'key3_2': 'value3_2'},
 'key4': 'something new'}

0

如果你想使用字典推导式来实现非常简洁的代码,可以使用以下方法。

注意:通过在if语句中使用.get(k),我们避免了检查k是否在字典中的步骤。

def merge_dicts(d1, d2):
    check = lambda k, v: isinstance(d1.get(k), dict) and isinstance(v, dict)
    return {**d1, **{k: merge_dicts(d1[k], d2[k]) if check(k, v) else v for k, v in d2.items()}}

输出:

>>> from pprint import pprint
>>> pprint(merge_dicts(a,b))
{'key1': 'not_key1',
 'key2': 'value2',
 'key3': {'key': 'new key without index?',
          'key3_1': 'Definitely not value3_1',
          'key3_2': 'value3_2'},
 'key4': 'something new'}

是的,这确实减少了代码行数,但并不高效。您正在迭代从两个列表创建的项目列表。这意味着您正在重新分配相同的值。如果目标字典有1000个项目,而增强字典只有一个项目,则会进行1001次迭代,而不是1次。 - Peter Trcka
你能试着改进一下吗?绝对是个好主意。 - Peter Trcka
@PeterTrcka 非常好的观点 - 我已经修改了我的解决方案,以拆开 d1 并仅循环遍历 d2 中的值。 - oskros
1
根据作者的说法,如果两个字典中都存在相同的键,则无需更新d1。我们只想从d2中增强d1的新键。如果您将v而不是d2传递给lambda,可以节省一些额外的时间。当我根据时间性能对您现有的增强进行了调整后,性能得到了改善-> 3s(原始)-> 1.5(您的第一个重构)-> 0.6(我的建议之后)。check = lambda k, v: isinstance(d1.get(k), dict) and isinstance(v, dict)和第二行:return {**d1, **{k: merge_dicts(d1[k], d2[k]) if check(k, v) else v for k, v in d2.items() if k not in d1}} - Peter Trcka

-2

这是一个更简单的解决方案。我相信你需要至少3.7+版本。

c = {**a, **b}

这不能递归合并。 - Altareos

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