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


162

我正在寻找一种方法来用字典更新内容覆盖字典字典A

dictionary1={'level1':{'level2':{'levelA':0,'levelB':1}}}
update={'level1':{'level2':{'levelB':10}}}
dictionary1.update(update)
print dictionary1
{'level1': {'level2': {'levelB': 10}}}

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

鉴于dictionary1和update可以有任何长度,我该如何解决?


嵌套始终是三层深度还是您可以嵌套任意深度?
ChristopheD

它可以具有任何深度/长度。
jay_t 2010年

如果我错了,请纠正我,但似乎这里的理想解决方案需要实现复合设计模式。
亚历山大·麦克努尔迪

Answers:


263

@FM的答案具有正确的总体思路,即递归解决方案,但有些特殊的编码和至少一个错误。我建议改为:

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

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

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

我的其他改动很小:没有理由让if/ else构造何时.get更快,更干净地完成相同的工作,并且isinstance为了通用起见,最好将其应用于抽象基类(而不是具体的基类)。


7
+1很好地抓住了臭虫-哎呀!我认为有人会有更好的方法来处理isinstance测试,但我认为我会采取行动。
FMc

6
另一个次要的“功能”会导致这种情况TypeError: 'int' object does not support item assignment.在您提出时出现,例如update({'k1': 1}, {'k1': {'k2': 2}})。要改变这种行为,而是扩大词典的深度,以腾出空间进行更深入的字典您可以添加elif isinstance(d, Mapping):周围d[k] = u[k]和之后isinstance的状态。您还需要添加一个,else: d = {k: u[k]}以处理更新字典比原始字典更深的情况。很高兴编辑答案,但不想弄脏解决OP问题的简洁代码。
滚刀

1
为什么使用isinstance(v, collections.Mapping)而不是isinstance(v, dict)?如果OP决定开始使用收藏集?
马特

2
@Matt Yea或任何其他映射派生的对象(事物对列表)。使函数更通用,并且更不可能安静地忽略映射派生的对象并使它们保持不更新(OP可能永远不会看到/捕获的隐患)。您几乎总是想使用Mapping查找dict类型,并使用basestring查找str类型。
滚刀

2
如果您是在Python 3+下运行此u.iteritems()命令u.items(),请更改为,否则会遇到:AttributeError: 'dict' object has no attribute 'iteritems'
Greg K

23

我花了一点时间,但是由于@Alex的帖子,他填补了我所缺少的空白。但是,如果递归中的值dict恰好是,我遇到了一个问题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

3
我想,这大概应该是(是有点更安全): orig_dict.get(key, []) + val
安迪·海登

2
由于字典是可变的,因此您将更改作为参数传递的实例。然后,您无需返回orig_dict。
gabrielhpugliese

3
我认为大多数人都希望该定义返回已更新的字典,即使该字典已被适当地更新。
凯尔·索拉尔

onosendi代码中的默认逻辑是将更新的列表附加到原始列表中。如果需要更新以覆盖原始列表,则需要设置orig_dict [key] = val
intijk

1
如果使用字典文字进行调用,则需要@gabrielhpugliese返回原始文件,例如 merged_tree = update({'default': {'initialvalue': 1}}, other_tree)
EoghanM

18

@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}}})

我懂了。您使我elif对原始对象类型的检查成为“包含”条件,其中包含对该dict / mapping的值和键的检查。聪明。
滚刀

如果内部字典具有多个键,则此方法将无效。
威林

@Wlerin,它仍然有效;届时d将成为Mapping。这是一个测试案例有多个键:update({'A1': 1, 'A2':2}, {'A1': {'B1': {'C1': 3, 'C2':4}, 'B2':2}, 'A3':5})。您是否有一个示例无法满足您的要求?
bscan

为什么要if isinstance(d, collections.Mapping)进行evey迭代测试?看我的回答
耶罗姆

13

与已接受的解决方案相同,但更清晰的变量命名,文档字符串,并修复了一个错误,{}即不能覆盖值的错误。

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

以下是一些测试案例:

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

该功能在charlatan软件包的中提供charlatan.utils


7

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

基于@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

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

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

1
谢谢你!depth参数可能适用于什么用例?
马特

@Matt,当您有一些不需要合并/更新的已知深度的对象/字典时,只是被新对象覆盖(例如用字典中的字符串或浮点数或其他内容替换字典)
滚刀

1
仅当更新深度比原始深度最多1级时,此方法才有效。例如,这失败了:update({'k1': 1}, {'k1': {'k2': {'k3': 3}}})我添加了一个解决此问题的答案
bscan

@bscan不错!从来没有想过那个用例。我想我应该在elif分支中更深入地递归。有任何想法吗?
滚刀

为什么要if isinstance(d, Mapping)进行evey迭代测试?看我的回答。(另外,我不知道你的d = {k: u[k]}
杰罗姆

4

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

而且,此解决方案不需要递归,这与dict.update()工作方式和return 更加对称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

            # get d[k], defaulting to a dict, if it doesn't previously
            # exist
            dv = d.setdefault(k, {})

            if not isinstance(dv, 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((dv, v))

4

只需使用python-benedict (我做到了),它就有一个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}}}

安装: pip install python-benedict

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


2

在这两个答案中,作者似乎都没有理解更新字典中存储的对象,甚至迭代字典项(与键相对)的概念。因此,我不得不写一篇不会造成毫无意义的重言式字典存储和检索的书。假定这些字典存储其他字典或简单类型。

def update_nested_dict(d, other):
    for k, v in other.items():
        if isinstance(v, collections.Mapping):
            d_v = d.get(k)
            if isinstance(d_v, collections.Mapping):
                update_nested_dict(d_v, v)
            else:
                d[k] = v.copy()
        else:
            d[k] = v

或更简单的一种适用于任何类型的:

def update_nested_dict(d, other):
    for k, v in other.items():
        d_v = d.get(k)
        if isinstance(v, collections.Mapping) and isinstance(d_v, collections.Mapping):
            update_nested_dict(d_v, v)
        else:
            d[k] = deepcopy(v) # or d[k] = v if you know what you're doing

2

更新@Alex Martelli的答案以修复其代码中的错误,以使解决方案更可靠:

def update_dict(d, u):
    for k, v in u.items():
        if isinstance(v, collections.Mapping):
            default = v.copy()
            default.clear()
            r = update_dict(d.get(k, default), v)
            d[k] = r
        else:
            d[k] = v
    return d

关键是我们经常想在递归时创建相同的类型,因此在这里我们使用v.copy().clear()not {}。如果这是可以具有不同类型s dict的类型collections.defaultdict,则这特别有用default_factory

另请注意,u.iteritems()已将更改为u.items()中的Python3


2

我使用了@Alex Martelli建议的解决方案,但失败了

TypeError 'bool' object does not support item assignment

当两个字典在某种程度上数据类型不同时。

在相同级别的情况下,字典的元素d只是一个标量(即Bool),而字典的元素u仍然是字典,则重新分配失败,因为无法将字典分配到标量中(如True[k])。

一个附加条件修复了:

from collections import Mapping

def update_deep(d, u):
    for k, v in u.items():
        # this condition handles the problem
        if not isinstance(d, Mapping):
            d = u
        elif isinstance(v, Mapping):
            r = update_deep(d.get(k, {}), v)
            d[k] = r
        else:
            d[k] = u[k]

    return d

2

下面的代码应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

1
def update(value, nvalue):
    if not isinstance(value, dict) or not isinstance(nvalue, dict):
        return nvalue
    for k, v in nvalue.items():
        value.setdefault(k, dict())
        if isinstance(v, dict):
            v = update(value[k], v)
        value[k] = v
    return value

使用dictcollections.Mapping


1

我知道这个问题已经很老了,但是当我不得不更新嵌套字典时,仍然会发布我的操作。我们可以使用dict通过python中的引用传递的事实,假设键的路径是已知的并且是点分隔的。外汇,如果我们有一个名为data的字典:

{
"log_config_worker": {
    "version": 1, 
    "root": {
        "handlers": [
            "queue"
        ], 
        "level": "DEBUG"
    }, 
    "disable_existing_loggers": true, 
    "handlers": {
        "queue": {
            "queue": null, 
            "class": "myclass1.QueueHandler"
        }
    }
}, 
"number_of_archived_logs": 15, 
"log_max_size": "300M", 
"cron_job_dir": "/etc/cron.hourly/", 
"logs_dir": "/var/log/patternex/", 
"log_rotate_dir": "/etc/logrotate.d/"
}

而且我们要更新队列类,键的路径将是- log_config_worker.handlers.queue.class

我们可以使用以下函数来更新值:

def get_updated_dict(obj, path, value):
    key_list = path.split(".")

    for k in key_list[:-1]:
        obj = obj[k]

    obj[key_list[-1]] = value

get_updated_dict(data, "log_config_worker.handlers.queue.class", "myclass2.QueueHandler")

这样可以正确更新字典。


1

可能是您偶然发现了像今天这样的非标准词典,该词典没有iteritems-Attribute。在这种情况下,很容易将这种类型的词典解释为标准词典。例如: Python 2.7:

    import collections
    def update(orig_dict, new_dict):
        for key, val in dict(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[key] + val)
            else:
                orig_dict[key] = new_dict[key]
        return orig_dict

    import multiprocessing
    d=multiprocessing.Manager().dict({'sample':'data'})
    u={'other': 1234}

    x=update(d, u)
    x.items()

Python 3.8:

    def update(orig_dict, new_dict):
        orig_dict=dict(orig_dict)
        for key, val in dict(new_dict).items():
            if isinstance(val, collections.abc.Mapping):
                tmp = update(orig_dict.get(key, { }), val)
                orig_dict[key] = tmp
            elif isinstance(val, list):
                orig_dict[key] = (orig_dict[key] + val)
            else:
                orig_dict[key] = new_dict[key]
        return orig_dict

    import collections
    import multiprocessing
    d=multiprocessing.Manager().dict({'sample':'data'})
    u={'other': 1234, "deeper": {'very': 'deep'}}

    x=update(d, u)
    x.items()

0

是! 和另一个解决方案。我的解决方案在要检查的键上有所不同。在所有其他解决方案中,我们仅查看中的键dict_b。但是这里我们看一下两个词典的结合。

随便做吧

def update_nested(dict_a, dict_b):
    set_keys = set(dict_a.keys()).union(set(dict_b.keys()))
    for k in set_keys:
        v = dict_a.get(k)
        if isinstance(v, dict):
            new_dict = dict_b.get(k, None)
            if new_dict:
                update_nested(v, new_dict)
        else:
            new_value = dict_b.get(k, None)
            if new_value:
                dict_a[k] = new_value

0

如果要用数组替换“完全嵌套的字典”,可以使用以下代码段:

它将用“ new_value”替换任何“ old_value”。它大致在对字典进行深度优先的重建。它甚至可以与作为第一级输入参数的List或Str / int一起使用。

def update_values_dict(original_dict, future_dict, old_value, new_value):
    # Recursively updates values of a nested dict by performing recursive calls

    if isinstance(original_dict, Dict):
        # It's a dict
        tmp_dict = {}
        for key, value in original_dict.items():
            tmp_dict[key] = update_values_dict(value, future_dict, old_value, new_value)
        return tmp_dict
    elif isinstance(original_dict, List):
        # It's a List
        tmp_list = []
        for i in original_dict:
            tmp_list.append(update_values_dict(i, future_dict, old_value, new_value))
        return tmp_list
    else:
        # It's not a dict, maybe a int, a string, etc.
        return original_dict if original_dict != old_value else new_value

0

使用递归的另一种方法:

def updateDict(dict1,dict2):
    keys1 = list(dict1.keys())
    keys2= list(dict2.keys())
    keys2 = [x for x in keys2 if x in keys1]
    for x in keys2:
        if (x in keys1) & (type(dict1[x]) is dict) & (type(dict2[x]) is dict):
            updateDict(dict1[x],dict2[x])
        else:
            dict1.update({x:dict2[x]})
    return(dict1)

0

一个新的Q如何通过钥匙链

dictionary1={'level1':{'level2':{'levelA':0,'levelB':1}},'anotherLevel1':{'anotherLevel2':{'anotherLevelA':0,'anotherLevelB':1}}}
update={'anotherLevel1':{'anotherLevel2':1014}}
dictionary1.update(update)
print dictionary1
{'level1':{'level2':{'levelA':0,'levelB':1}},'anotherLevel1':{'anotherLevel2':1014}}

0

您可以尝试一下,它可以与列表一起使用,而且很纯粹:

def update_keys(newd, dic, mapping):
  def upsingle(d,k,v):
    if k in mapping:
      d[mapping[k]] = v
    else:
      d[k] = v
  for ekey, evalue in dic.items():
    upsingle(newd, ekey, evalue)
    if type(evalue) is dict:
      update_keys(newd, evalue, mapping)
    if type(evalue) is list:
      upsingle(newd, ekey, [update_keys({}, i, mapping) for i in evalue])
  return newd

0

我建议,以取代{}通过type(v)()以存储在任何字典子类的繁殖对象类型,u但不存在d。例如,这将保留诸如collections.OrderedDict之类的类型:

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, type(v)()), v)
        else:
            d[k] = v
    return d

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, type(v)()), v)
        else:
            d[k] = v
    return d

-1

这有点偏,但是您真的需要嵌套字典吗?根据问题的不同,有时平字典可能就足够了……并且看起来很不错:

>>> dict1 = {('level1','level2','levelA'): 0}
>>> dict1['level1','level2','levelB'] = 1
>>> update = {('level1','level2','levelB'): 10}
>>> dict1.update(update)
>>> print dict1
{('level1', 'level2', 'levelB'): 10, ('level1', 'level2', 'levelA'): 0}

5
嵌套结构来自传入的json数据集,因此我想保持它们完整无缺,……
jay_t 2010年

-1

如果您想要单线:

{**dictionary1, **{'level1':{**dictionary1['level1'], **{'level2':{**dictionary1['level1']['level2'], **{'levelB':10}}}}}}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.