Обновить значение вложенного словаря различной глубины
Я ищу способ обновить dict dictionary1 с содержимым dict update wihout переписывая levelA
dictionary1={'level1':{'level2':{'levelA':0,'levelB':1}}}
update={'level1':{'level2':{'levelB':10}}}
dictionary1.update(update)
print dictionary1
{'level1': {'level2': {'levelB': 10}}}
Я знаю, что обновление удаляет значения в level2, потому что он обновляет самый низкий уровень ключа1.
Как я мог решить это, учитывая, что словарь1 и обновление могут иметь любую длину?
Ответы
Ответ 1
Ответ @FM имеет правильную общую идею, то есть рекурсивное решение, но несколько своеобразное кодирование и по крайней мере одну ошибку. Вместо этого я бы порекомендовал:
import collections
import six
# python 3.8+ compatibility
try:
collectionsAbc = collections.abc
except:
collectionsAbc = collections
def update(d, u):
for k, v in six.iteritems(u):
dv = d.get(k, {})
if not isinstance(dv, collectionsAbc.Mapping):
d[k] = v
elif isinstance(v, collectionsAbc.Mapping):
d[k] = update(dv, v)
else:
d[k] = v
return d
Ошибка появляется, когда "обновление" содержит элемент k
, v
, где v
является dict
, а k
изначально не является ключом в обновляемом словаре - код @FM "пропускает" эту часть обновления (поскольку она выполняет ее на пустом новом dict
, который нигде не сохраняется и не возвращается, просто теряется при возврате рекурсивного вызова).
Мои другие изменения незначительны: нет никакой причины для конструкции if
/else
, когда .get
выполняет ту же работу быстрее и чище, а isinstance
лучше всего применять для абстрактных базовых классов (не конкретных) для универсальности ,
Ответ 2
Принял меня немного на этом, но, благодаря сообщению @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
@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}}})
Ответ 4
То же решение, что и принятое, но более четкое именование переменных, строка документации и исправлена ошибка, при которой {}
в качестве значения не переопределяло бы.
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
.
Ответ 5
Незначительные улучшения @Alex answer, который позволяет обновлять словари различной глубины, а также ограничивать глубину, которую обновление погружает в исходный вложенный словарь (но обновление глубина словаря не ограничена). Проверено только несколько случаев:
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
Ответ 6
Здесь непреложная версия рекурсивного словаря объединяется, если кому-то это нужно.
Основываясь на @Alex Martelli answer.
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
Ответ 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
# 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))
Ответ 8
В любом из этих ответов авторы, похоже, понимают концепцию обновления объекта, хранящегося в словаре, и даже итерации по элементам словаря (в отличие от ключей). Поэтому я должен был написать тот, который не делает бессмысленные тавтологические словарные магазины и извлечения.
Предполагается, что dicts хранит другие dicts или простые типы.
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
Ответ 9
Обновите ответ @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
, который может иметь разные типы default_factory
s.
Также обратите внимание, что u.iteritems()
был изменен на u.items()
в Python3
.
Ответ 10
Я использовал решение @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
Ответ 11
Возможно, вы натыкаетесь на нестандартный словарь, как я сегодня, у которого нет iteritems-Attribute.
В этом случае легко интерпретировать этот тип словаря как стандартный словарь. Например:.
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()
Ответ 12
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
использовать dict
или collections.Mapping
Ответ 13
Я знаю, что этот вопрос довольно старый, но я все еще публикую то, что я делаю, когда мне нужно обновить вложенный словарь. Мы можем использовать тот факт, что дикты передаются по ссылке в Python
Предполагая, что путь к ключу известен и разделен точками. Форекс, если у нас есть данные с именем dict:
{
"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")
Это правильно обновит словарь.
Ответ 14
Просто используйте 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
Ответ 15
Если вы хотите заменить "полный вложенный словарь массивами", вы можете использовать этот фрагмент:
Он заменит любое "old_value" на "new_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 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 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 not a dict, maybe a int, a string, etc.
return original_dict if original_dict != old_value else new_value
Ответ 16
Да! И еще одно решение. Мое решение отличается ключами, которые проверяются.
Во всех других решениях мы смотрим только на клавиши в 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
Ответ 17
Если вы хотите однострочник:
{**dictionary1, **{'level1':{**dictionary1['level1'], **{'level2':{**dictionary1['level1']['level2'], **{'levelB':10}}}}}}
Ответ 18
Это немного в сторону, но вам действительно нужны вложенные словари? В зависимости от проблемы иногда может быть плоский словарь... и хорошо смотря на него:
>>> 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}