Значение вложенного словаря различной глубины
Я ищу способ, чтобы обновить дикт dictionary1 с содержанием обновления дикт без перезаписи лэвэла
dictionary1={'level1':{'level2':{'levelA':0,'levelB':1}}}
update={'level1':{'level2':{'levelB':10}}}
dictionary1.update(update)
print dictionary1
{'level1': {'level2': {'levelB': 10}}}
Я знаю, что обновление удаляет значения в level2, потому что это обновление самого низкого ключевого level1.
Как я могу решить эту проблему, учитывая, что dictionary1 и update могут иметь любую длину?
12 ответов:
ответ@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 dPython 3:
import collections def update(d, u): for k, v in u.items(): if isinstance(v, collections.Mapping): d[k] = update(d.get(k, {}), v) else: d[k] = v return dошибка появляется, когда "обновление" имеет
k,vпункт гдеv- этоdictиkизначально не является ключом в обновляемом словаре -- код @FM "пропускает" эту часть обновления (потому что он выполняет его на пустом новыйdictкоторый нигде не сохраняется и не возвращается, просто теряется при возврате рекурсивного вызова).мои другие изменения незначительны: нет никаких причин для
if/elseстроительство, когда.getделает ту же работу быстрее и чище, иisinstanceлучше всего применяется к абстрактным базовым классам (не конкретным) для общности.
взял меня немного на этом, но благодаря сообщению @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
ответ@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(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
то же решение, что и принятое, но более четкое имя переменной, 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вот несколько тестов:
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.
вот неизменяемая версия рекурсивного словаря слияния в случае, если кто-то нуждается в нем.
на основе @Алекс Мартелли это ответ.
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 resultPython 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
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, которые могут иметь разные видыdefault_factorys.также обратите внимание, что
u.iteritems()было изменено наu.items()наPython3.
я использовал решение @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
возможно, вы спотыкаетесь о нестандартный словарь, как я сегодня, который не имеет атрибута iteritems. В этом случае легко интерпретировать этот тип словаря как стандартный словарь. Например:
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()
этот вопрос старый, но я приземлился здесь при поиске решения "глубокого слияния". Ответы выше вдохновили то, что следует. Я закончил писать свой собственный, потому что были ошибки во всех версиях, которые я тестировал. Критическая точка пропущена была, на некоторой произвольной глубине двух входных диктов, для некоторого ключа, k, дерева решений, когда d[k] или u[k]не дикт был неисправен.
кроме того, это решение не требует рекурсии, которая более симметрична с how
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))
Это немного в сторону, но вам действительно нужны вложенные словари? В зависимости от проблемы, иногда плоский словарь может быть достаточно... и посмотрите хорошенько на это:
>>> 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}
Comments