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

Я ищу способ обновить 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}