Значение вложенного словаря различной глубины



Я ищу способ, чтобы обновить дикт 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 могут иметь любую длину?

211   12  

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 d

Python 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 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
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_factory s.

также обратите внимание, что 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

    Ничего не найдено.