更新不同深度的嵌套字典的值

jay*_*y_t 150 python

我正在寻找一种方法来更新dict dictionary1与dict更新的内容wihout覆盖levelA

dictionary1={'level1':{'level2':{'levelA':0,'levelB':1}}}
update={'level1':{'level2':{'levelB':10}}}
dictionary1.update(update)
print dictionary1
{'level1': {'level2': {'levelB': 10}}}
Run Code Online (Sandbox Code Playgroud)

我知道更新会删除level2中的值,因为它正在更新最低密钥level1.

鉴于dictionary1和update可以有任何长度,我怎么能解决这个问题呢?

Ale*_*lli 246

@ FM的答案有正确的总体思路,即递归解决方案,但有些特殊的编码和至少一个bug.我推荐,相反:

Python 2:

import collections

def update(d, u):
    for k, v in u.iteritems():
        if isinstance(v, collections.Mapping):
            d[k] = update(d.get(k, {}), v)
        else:
            d[k] = v
    return d
Run Code Online (Sandbox Code Playgroud)

Python 3:

import collections.abc

def update(d, u):
    for k, v in u.items():
        if isinstance(v, collections.abc.Mapping):
            d[k] = update(d.get(k, {}), v)
        else:
            d[k] = v
    return d
Run Code Online (Sandbox Code Playgroud)

该错误时显示"更新"有k,v项目在那里vdictk最初不是被更新在字典中的关键- @ FM代码"跳过"更新的这一部分(因为它执行它的新的空dict其没有保存或返回任何地方,只是在递归调用返回时丢失).

我的其他更改是次要的:没有理由if/ elseconstruct什么时候.get同一个工作更快更干净,并且isinstance最好应用于抽象基类(不是具体的)以获得通用性.

  • 另一个次要的"特性"导致它引发`TypeError:'int'对象不支持项目赋值.当你,例如`update({'k1':1},{'k1':{'k2':2} })`.要改变这种行为,而是扩展词典的深度以便为更深层的词典腾出空间,你可以添加一个`elif isinstance(d,Mapping):`围绕`d [k] = u [k]`并在`isinstance之后`条件.你还需要添加一个`else:d = {k:u [k]}来处理更新字典比原始字典更深的情况.很高兴编辑答案,但不想弄脏简洁的代码来解决OP的问题. (6认同)
  • +1很好的捕获bug - doh!我想有人会有一个更好的方法来处理`isinstance`测试,但我想我会抓住它. (5认同)
  • 为什么使用“isinstance(v, collections.Mapping)”而不是“isinstance(v, dict)”?如果 OP 决定开始使用集合呢? (3认同)
  • @Matt Yea,或任何其他映射派生对象(成对事物列表).使函数更通用,更不可能静静地忽略映射派生对象并使它们不被更新(OP可能看不到/捕获的潜在错误).您几乎总是希望使用Mapping来查找dict类型和basestring来查找str类型. (2认同)
  • 仅当旧值和新值都是集合时才需要递归:`if isinstance(d.get(k, None), collections.Mapping) 和 isinstance(v, collections.Mapping): d[k] = update(d[ k], v)` 后跟 `else: d[k] = v` (2认同)
  • 如果您是在Python 3+上运行此命令,请将u.iteritems()更改为u.items(),否则会遇到:AttributeError:“ dict”对象没有属性“ iteritems” (2认同)
  • 请注意, `update({'k1': None}, {'k1': {'foo': 'bar'})` 会导致崩溃,而我期望的是 `{'k1': {'foo' : '酒吧'}`。 (2认同)
  • @paulmelnikow,用字典替换整数的问题应该由我下面的答案解决 (2认同)
  • @hobs我认为如果我们将行:`d[k] = update(d.get(k, {}), v)`替换为:`d[k] = update({}, v),这将是最简单的)`。也就是说,传递一个**空字典**,它将在下一轮用新数据填充。这必须解决上面评论中的问题。 (2认同)
  • @hobs 我将解决方案发布为新答案,因为不允许编辑此答案。我已经测试过代码了。效果很好。享受)/sf/answers/4448077721/ (2认同)

kep*_*ler 63

如果您碰巧正在使用pydantic(很棒的库,顺便说一句),您可以使用它的实用方法之一:

from pydantic.utils import deep_update

dictionary1 = deep_update(dictionary1, update)
Run Code Online (Sandbox Code Playgroud)

更新:对 code 的引用,如@Jorgu所指出的。如果不需要安装 pydantic,只要有足够的许可证兼容性,代码就足够短,可以复制。

  • 这应该赞成。大多数人现在应该都在使用这个。不需要自己烘焙这个实现 (12认同)
  • [Github 上的 pydantic.utils.deep_update](https://github.com/samuelcolvin/pydantic/blob/9d631a3429a66f30742c1a52c94ac18ec6ba848d/pydantic/utils.py#L198) TLDR:它是递归的、类型化的,并且同时接受多个更新 (4认同)
  • FWIW,pydantic似乎在V2中“弃用”了这个(移至v1子包并将其放入文档中的弃用表中,尽管它没有提出任何警告并且似乎没有任何结论性信息关于何时/是否真正将其删除。) (2认同)
  • @vacky FWIW 根据[此讨论](https://github.com/pydantic/pydantic/discussions/7505),pydantic 将保留 `V1` 直到 `V3`。我仍在使用“V1”作为“BaseSettings”(当“v1.BaseSettings”工作正常时,我不想依赖“pydantic-settings”),现在我可能也会使用这个“deep_update”。 (2认同)

小智 22

对我说了一点,但多亏了@ Alex的帖子,他填补了我所缺少的空白.但是,如果递归中的值dict恰好是a ,我遇到了一个问题list,所以我想我会分享,并扩展他的答案.

import collections

def update(orig_dict, new_dict):
    for key, val in new_dict.iteritems():
        if isinstance(val, collections.Mapping):
            tmp = update(orig_dict.get(key, { }), val)
            orig_dict[key] = tmp
        elif isinstance(val, list):
            orig_dict[key] = (orig_dict.get(key, []) + val)
        else:
            orig_dict[key] = new_dict[key]
    return orig_dict
Run Code Online (Sandbox Code Playgroud)

  • 我认为这应该是(更安全一点):`orig_dict.get(key,[])+ val`. (3认同)
  • 我想大多数人都希望定义能够返回更新后的dict,即使它已经更新到位. (3认同)
  • 由于dicts是可变的,因此您正在更改作为参数传递的实例.然后,您不需要返回orig_dict. (2认同)
  • @gabrielhpugliese 如果使用字典文字调用,则需要返回原始内容,例如 `merged_tree = update({'default': {'initialvalue': 1}}, other_tree)` (2认同)

bsc*_*can 16

@Alex的答案很好,但是当用字典替换整数等元素时不起作用,例如update({'foo':0},{'foo':{'bar':1}}).此更新解决了它:

import collections
def update(d, u):
    for k, v in u.iteritems():
        if isinstance(d, collections.Mapping):
            if isinstance(v, collections.Mapping):
                r = update(d.get(k, {}), v)
                d[k] = r
            else:
                d[k] = u[k]
        else:
            d = {k: u[k]}
    return d

update({'k1': 1}, {'k1': {'k2': {'k3': 3}}})
Run Code Online (Sandbox Code Playgroud)

  • 当您已经有v时,为什么要使用u [k]?否则,+ 1。 (3认同)

cha*_*lax 12

与接受的解决方案相同的解决方案,但更清晰的变量命名,docstring,并修复了一个错误,其中{}值不会覆盖.

import collections


def deep_update(source, overrides):
    """
    Update a nested dictionary or similar mapping.
    Modify ``source`` in place.
    """
    for key, value in overrides.iteritems():
        if isinstance(value, collections.Mapping) and value:
            returned = deep_update(source.get(key, {}), value)
            source[key] = returned
        else:
            source[key] = overrides[key]
    return source
Run Code Online (Sandbox Code Playgroud)

以下是一些测试用例:

def test_deep_update():
    source = {'hello1': 1}
    overrides = {'hello2': 2}
    deep_update(source, overrides)
    assert source == {'hello1': 1, 'hello2': 2}

    source = {'hello': 'to_override'}
    overrides = {'hello': 'over'}
    deep_update(source, overrides)
    assert source == {'hello': 'over'}

    source = {'hello': {'value': 'to_override', 'no_change': 1}}
    overrides = {'hello': {'value': 'over'}}
    deep_update(source, overrides)
    assert source == {'hello': {'value': 'over', 'no_change': 1}}

    source = {'hello': {'value': 'to_override', 'no_change': 1}}
    overrides = {'hello': {'value': {}}}
    deep_update(source, overrides)
    assert source == {'hello': {'value': {}, 'no_change': 1}}

    source = {'hello': {'value': {}, 'no_change': 1}}
    overrides = {'hello': {'value': 2}}
    deep_update(source, overrides)
    assert source == {'hello': {'value': 2, 'no_change': 1}}
Run Code Online (Sandbox Code Playgroud)

这个功能在charlatan包中可用charlatan.utils.

  • 迷人的。但必须在 Python 3.9+ 上将 `overrides.iteritems()` 更新为 `overrides.items()`,将 `collections.Mapping` 更新为 `collections.abc.Mapping` (6认同)

djp*_*nne 7

这个问题很老,但我在寻找“深度合并”解决方案时来到这里。上面的答案启发了接下来的内容。我最终写了自己的,因为我测试的所有版本都存在错误。错过的关键点是,在两个输入字典的任意深度,对于某些键 k,当 d[k] 或 u[k]不是字典时,决策树是错误的。

此外,此解决方案不需要递归,这与dict.update()工作原理更加对称,并返回None.

import collections
def deep_merge(d, u):
   """Do a deep merge of one dict into another.

   This will update d with values in u, but will not delete keys in d
   not found in u at some arbitrary depth of d. That is, u is deeply
   merged into d.

   Args -
     d, u: dicts

   Note: this is destructive to d, but not u.

   Returns: None
   """
   stack = [(d,u)]
   while stack:
      d,u = stack.pop(0)
      for k,v in u.items():
         if not isinstance(v, collections.Mapping):
            # u[k] is not a dict, nothing to merge, so just set it,
            # regardless if d[k] *was* a dict
            d[k] = v

        else:
            # note: u[k] is a dict
            if k not in d:
                # add new key into d
                d[k] = v
            elif not isinstance(d[k], collections.Mapping):
                # d[k] is not a dict, so just set it to u[k],
                # overriding whatever it was
                d[k] = v
            else:
                # both d[k] and u[k] are dicts, push them on the stack
                # to merge
                stack.append((d[k], v))
Run Code Online (Sandbox Code Playgroud)


hob*_*obs 6

@ Alex的答案进行了一些小改进,可以更新不同深度的字典,并限制更新深入到原始嵌套字典中的深度(但更新字典深度不受限制).只有少数案例经过测试:

def update(d, u, depth=-1):
    """
    Recursively merge or update dict-like objects. 
    >>> update({'k1': {'k2': 2}}, {'k1': {'k2': {'k3': 3}}, 'k4': 4})
    {'k1': {'k2': {'k3': 3}}, 'k4': 4}
    """

    for k, v in u.iteritems():
        if isinstance(v, Mapping) and not depth == 0:
            r = update(d.get(k, {}), v, depth=max(depth - 1, -1))
            d[k] = r
        elif isinstance(d, Mapping):
            d[k] = u[k]
        else:
            d = {k: u[k]}
    return d
Run Code Online (Sandbox Code Playgroud)

  • 谢谢你!深度参数可能适用于什么用例? (2认同)
  • 仅当更新最多比原始深度深 1 级时,此方法才有效。例如,这会失败: `update({'k1': 1}, {'k1': {'k2': {'k3': 3}}})` 我添加了一个解决此问题的答案 (2认同)
  • 为什么在每次迭代时测试“if isinstance(d, Mapping)”?请参阅[我的答案](/sf/answers/4222528341/)。(另外,我不确定你的`d = {k: u[k]}`) (2认同)
  • 我正在使用霍布斯的答案,但遇到了更新字典比原始字典更深的情况,杰罗姆的答案对我有用! (2认同)

kab*_*hya 6

如果有人需要,这是递归字典合并的不可变版本。

基于@Alex Martelli的答案

Python 2.x:

import collections
from copy import deepcopy


def merge(dict1, dict2):
    ''' Return a new dictionary by merging two dictionaries recursively. '''

    result = deepcopy(dict1)

    for key, value in dict2.iteritems():
        if isinstance(value, collections.Mapping):
            result[key] = merge(result.get(key, {}), value)
        else:
            result[key] = deepcopy(dict2[key])

    return result
Run Code Online (Sandbox Code Playgroud)

Python 3.x:

import collections
from copy import deepcopy


def merge(dict1, dict2):
    ''' Return a new dictionary by merging two dictionaries recursively. '''

    result = deepcopy(dict1)

    for key, value in dict2.items():
        if isinstance(value, collections.Mapping):
            result[key] = merge(result.get(key, {}), value)
        else:
            result[key] = deepcopy(dict2[key])

    return result
Run Code Online (Sandbox Code Playgroud)


Fab*_*amo 6

只需使用python-benedict (I did it),它就有一个merge(deepupdate) 实用程序方法和许多其他方法。它适用于 python 2 / python 3,并且经过了很好的测试。

from benedict import benedict

dictionary1=benedict({'level1':{'level2':{'levelA':0,'levelB':1}}})
update={'level1':{'level2':{'levelB':10}}}
dictionary1.merge(update)
print(dictionary1)
# >> {'level1':{'level2':{'levelA':0,'levelB':10}}}
Run Code Online (Sandbox Code Playgroud)

安装: pip install python-benedict

文档:https : //github.com/fabiocaccamo/python-benedict

注意:我是这个项目的作者


Jér*_*ôme 5

下面的代码应该以update({'k1': 1}, {'k1': {'k2': 2}})正确的方式解决@Alex Martelli 的答案中的问题。

def deepupdate(original, update):
    """Recursively update a dict.

    Subdict's won't be overwritten but also updated.
    """
    if not isinstance(original, abc.Mapping):
        return update
    for key, value in update.items():
        if isinstance(value, abc.Mapping):
            original[key] = deepupdate(original.get(key, {}), value)
        else:
            original[key] = value
    return original
Run Code Online (Sandbox Code Playgroud)