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

我正在寻找一种方法来更新字典dictionary1与字典更新的内容,而不覆盖levelA

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可以有任何长度,我该如何解决这个问题?

161676 次浏览

@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项,其中vdict,而k在被更新的字典中最初不是一个键时,这个错误就会出现——@FM的代码“跳过”更新的这一部分(因为它在一个空的新的dict上执行,它没有保存或返回任何地方,只是在递归调用返回时丢失)。

我的其他变化很小:没有理由使用if/else构造,因为.get可以更快更清楚地完成同样的工作,而且出于一般性考虑,isinstance最好应用于抽象基类(而不是具体基类)。

这有点离题了,但是你真的需要嵌套字典吗?根据问题的不同,有时候扁平的字典可能就足够了…好好看看:

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

@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

在这个问题上,我花了一点时间,但多亏了@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

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

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.utils中的江湖骗子包中可用。

可能是你偶然发现了一个非标准字典,就像我今天一样,它没有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()

@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}}})
在这两个答案中,作者似乎都不理解更新存储在字典中的对象的概念,甚至也不理解遍历字典项(而不是键)。所以我必须写一个不做无意义的重复字典存储和检索。 字典被假定存储其他字典或简单类型
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

更新@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()而不是{}。如果这里的dict类型为collections.defaultdict,可以有不同类型的__abc4,则这尤其有用。

还要注意,u.iteritems()Python3中已被更改为u.items()

我使用了@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

这里有一个递归字典合并的不可变版本,以防有人需要它。

基于@Alex Martelli的回答

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

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

这个问题很老了,但我是在搜索“深度合并”的时候来到这里的。解决方案。上面的答案启发了下面的内容。最后我自己写了,因为我测试的所有版本都有bug。错过的临界点是,在两个输入字典的任意深度,对于某个键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))
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

我知道这个问题很老了,但当我必须更新一个嵌套字典时,我仍然会发布我所做的事情。我们可以利用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")

这将正确地更新字典。

如果你想用数组替换一个“全嵌套字典”,你可以使用这个代码片段:

它将用“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
< p >是的!另一个解。我的解决方案在被检查的键上有所不同。 在所有其他解决方案中,我们只查看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

只需使用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

注:我是这个项目的作者

另一种使用递归的方法:

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)

一个新的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}}

你可以试试这个,它适用于列表并且是纯的:

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

我建议将{}替换为type(v)(),以便传播存储在u中但在d中不存在的任何dict子类的对象类型。例如,这将保留集合等类型。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

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

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

感谢滚铣刀亚历克斯的 回答的评论。的确,update({'k1': 1}, {'k1': {'k2': 2}})将导致TypeError: 'int' object does not support item assignment.

我们应该在函数的开头检查输入值的类型。因此,我建议使用下面的函数,它应该可以解决这个(和其他)问题。

Python 3:

from collections.abc import Mapping




def deep_update(d1, d2):
if all((isinstance(d, Mapping) for d in (d1, d2))):
for k, v in d2.items():
d1[k] = deep_update(d1.get(k), v)
return d1
return d2

我做了一个简单的函数,其中你给出键,新的值和字典作为输入,它递归地更新它的值:

def update(key,value,dictionary):
if key in dictionary.keys():
dictionary[key] = value
return
dic_aux = []
for val_aux in dictionary.values():
if isinstance(val_aux,dict):
dic_aux.append(val_aux)
for i in dic_aux:
update(key,value,i)
for [key2,val_aux2] in dictionary.items():
if isinstance(val_aux2,dict):
dictionary[key2] = val_aux2


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


#output: {'level1': {'level2': {'levelA': 0, 'levelB': 10}}}

希望答案是肯定的。

如果你碰巧在使用pydantic(很棒的库,顺便说一句),你可以使用它的一个实用程序方法:

from pydantic.utils import deep_update


dictionary1 = deep_update(dictionary1, update)

更新:代码引用,正如@Jorgu所指向的。如果不需要安装pydantic,只要提供足够的许可兼容性,代码就足够短,可以复制。

原创答案来源:@Gustavo Alves Casqueiro

老实说,我更喜欢使用一个可以为我做繁重工作的库,但我就是找不到一个能满足我需要的库。

我只在这个函数中添加了几个额外的检查。

我在dict中包含了对lists的检查,并为嵌套dict的名称添加了一个参数,以便在OUTER dict中有另一个具有相同名称的<强> < / >强可能时正确更新嵌套的dict KEY。

更新功能:

def update(dictionary: dict[str, any], key: str, value: any, nested_dict_name: str = None) -> dict[str, any]:
if not nested_dict_name:  # if current (outermost) dict should be updated
if key in dictionary.keys():  # check if key exists in current dict
dictionary[key] = value
return dictionary
else:  # if nested dict should be updated
if nested_dict_name in dictionary.keys():  # check if dict is in next layer
if isinstance(dictionary[nested_dict_name], dict):
if key in dictionary[nested_dict_name].keys():  # check if key exists in current dict
dictionary[nested_dict_name][key] = value
return dictionary
if isinstance(dictionary[nested_dict_name], list):
list_index = random.choice(range(len(dictionary[nested_dict_name])))  # pick a random dict from the list


if key in dictionary[nested_dict_name][list_index].keys():  # check if key exists in current dict
dictionary[nested_dict_name][list_index][key] = value
return dictionary


dic_aux = []


# this would only run IF the above if-statement was not able to identity and update a dict
for val_aux in dictionary.values():
if isinstance(val_aux, dict):
dic_aux.append(val_aux)


# call the update function again for recursion
for i in dic_aux:
return update(dictionary=i, key=key, value=value, nested_dict_name=nested_dict_name)

原来的东西:

{
"level1": {
"level2": {
"myBool": "Original",
"myInt": "Original"
},
"myInt": "Original",
"myBool": "Original"
},
"myStr": "Original",
"level3": [
{
"myList": "Original",
"myInt": "Original",
"myBool": "Original"
}
],
"level4": [
{
"myList": "Original",
"myInt": "UPDATED",
"myBool": "Original"
}
],
"level5": {
"level6": {
"myBool": "Original",
"myInt": "Original"
},
"myInt": "Original",
"myBool": "Original"
}
}

用于更新的数据(使用pytest):

@pytest.fixture(params=[(None, 'myStr', 'UPDATED'),
('level1', 'myInt', 'UPDATED'),
('level2', 'myBool', 'UPDATED'),
('level3', 'myList', 'UPDATED'),
('level4', 'myInt', 'UPDATED'),
('level5', 'myBool', 'UPDATED')])
def sample_data(request):
return request.param

'UPDATED'参数在这个较小的用例中没有意义(因为我可以硬编码它),但为了简单起见,在读取日志时,我不想看到多种数据类型,只是让它显示一个'UPDATED'字符串。

测试:

@pytest.mark.usefixtures('sample_data')
def test_this(sample_data):
nested_dict, param, update_value = sample_data


if nested_dict is None:
print(f'\nDict Value: Level0\nParam: {param}\nUpdate Value: {update_value}')
else:
print(f'\nDict Value: {nested_dict}\nParam: {param}\nUpdate Value: {update_value}')


# initialise data dict
data_object = # insert data here (see example dict above)


# first print as is
print(f'\nOriginal Dict:\n{data_object}')


update(dictionary=data_object,
key=param,
value=update_value,
nested_dict_name=nested_dict)


# print updated
print(f'\nUpdated Dict:\n{data_object}')

有一个警告,当你有这样的字典时:

{
"level1": {
"level2": {
"myBool": "Original"
},
"myBool": "Original"
},
"level3": {
"level2": {
"myBool": "Original"
},
"myInt": "Original"
}
}

其中level2level1level3下面。这将需要使用list或带有nested_dict_name的东西,并传入外部dict和内部dict (['level5', 'level2'])的名称,然后以某种方式遍历值以找到dict

然而,由于我还没有< >强然而< / >强遇到我使用的数据对象的这个问题,我还没有花时间试图解决这个“问题”。

将你的字典转换为NestedDict

from ndicts.ndicts import NestedDict


dictionary1 = {'level1': {'level2': {'levelA': 0, 'levelB': 1}}}
update = {'level1': {'level2': {'levelB': 10}}}


nd, nd_update = NestedDict(dictionary1), NestedDict(update)

然后使用update

>>> nd.update(nd_update)
>>> nd
NestedDict({'level1': {'level2': {'levelA': 0, 'levelB': 10}}})

如果你需要结果作为字典nd.to_dict()

安装ndicts pip install ndicts

d是要更新的字典,u是字典更新器。

def recursively_update_dict(d: dict, u: dict):
for k, v in u.items():
if isinstance(v, dict):
d.setdefault(k, {})
recursively_update_dict(d[k], v)
else:
d[k] = v

或defaultdict

from collections import defaultdict


def recursively_update_defaultdict(d: defaultdict[dict], u: dict):
for k, v in u.items():
if isinstance(v, dict):
recursively_update_dict(d[k], v)
else:
d[k] = v