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

163

Я ищу способ обновить dict dictionary1 содержимым dict update без перезаписи уровняA

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

Я знаю, что обновление удаляет значения в level2, потому что оно обновляет самый низкий ключ level1.

Как я могу решить это, учитывая, что dictionary1 и обновление могут иметь любую длину?

jay_t
источник
Вложение всегда на три уровня глубиной или вы можете иметь вложение произвольной глубины?
ChristopheD
Может иметь любую глубину / длину.
jay_t
Поправьте меня, если я ошибаюсь, но кажется, что идеальное решение здесь требует реализации составного шаблона проектирования.
Александр МакНалти
новый Q stackoverflow.com/questions/59004746/...
user7337353

Ответы:

264

Ответ @ 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

Показывает ошибку вверх , когда «обновление» есть k, vпункт , где vявляется dictи kне является изначально ключом в словаре обновляется - @ код «скачет» FM в этой части обновления (поскольку он выполняет его на пустом новом , dictкоторый нигде не сохраняется и не возвращается, просто теряется при возврате рекурсивного вызова).

Мои другие изменения незначительны: нет никакой причины для конструкции if/, elseкогда .getта же самая работа выполняется быстрее и чище, и isinstanceее лучше всего применять для абстрактных базовых классов (не конкретных) для общности.

Алекс Мартелли
источник
7
+1 Хороший улов на баге - дох! Я isinstanceдумал, что у кого-то будет лучший способ справиться с тестом, но подумал, что мне нужно сделать на него удар.
FMC
6
Еще одна незначительная «особенность» вызывает это, TypeError: 'int' object does not support item assignment.когда вы, например update({'k1': 1}, {'k1': {'k2': 2}}). Чтобы изменить это поведение и вместо этого расширить глубину словарей, чтобы освободить место для более глубоких словарей, вы можете добавить elif isinstance(d, Mapping):вокруг d[k] = u[k]и после isinstanceусловия. Вам также нужно будет добавить, else: d = {k: u[k]}чтобы иметь дело со случаем, когда обновленный dict глубже, чем исходный. Рад редактировать ответ, но не хочу испачкать краткий код, который решает проблему ОП.
конфорки
1
Почему использовать, isinstance(v, collections.Mapping)а не isinstance(v, dict)? В случае, если OP решит начать использовать коллекции?
Мэтт
2
@Matt Да, или любой другой объект, производный от отображения (списки пар вещей). Делает функцию более общей и снижает вероятность незаметного игнорирования объектов, производных от отображения, и оставляет их необновленными (коварная ошибка, которую OP может никогда не увидеть / не отловить). Вы почти всегда хотите использовать Mapping для поиска типов dict и basestring для поиска типов str.
варенье
2
Если вы работаете в Python 3+, измените u.iteritems()на u.items(), иначе вы встретите:AttributeError: 'dict' object has no attribute 'iteritems'
Грег К
23

Взял меня немного на этот, но благодаря посту @ Алекса, он заполнил пробел, который я пропустил. Однако, я столкнулся с проблемой, если значение в рекурсиве dictоказалось a 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
Я думаю, что это должно быть (быть немного безопаснее) orig_dict.get(key, []) + val.
Энди Хейден
2
Поскольку dicts изменчивы, вы меняете экземпляр, который вы передаете в качестве аргумента. Тогда вам не нужно возвращать orig_dict.
gabrielhpugliese
3
Я думаю, что большинство людей ожидают, что определение возвратит обновленный dict, даже если оно обновлено на месте.
Кел Солаар
Логика по умолчанию в коде onosendi заключается в добавлении обновленного списка в исходный список. Если вам нужно обновить перезаписать исходный список, вам нужно установить orig_dict [key] = val
intijk
1
@gabrielhpugliese возвращение оригинала необходимо, если вызывается с литералом словаря, например merged_tree = update({'default': {'initialvalue': 1}}, other_tree)
EoghanM
18

@ Ответ Алекса хорош, но не работает при замене элемента, такого как целое число, словарем, например 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}}})
BSCAN
источник
Понимаю. Вы сделали мою elifпроверку исходного типа объекта условным "включающим", содержащим проверки как значения, так и ключа этого dict / mapping. Умная.
варенье
Это не сработает, если внутренний дикт имеет более одного ключа.
Wlerin
@ Wlerin, это все еще работает; К этому моменту d станет Картографией. Вот тест с несколькими ключами: update({'A1': 1, 'A2':2}, {'A1': {'B1': {'C1': 3, 'C2':4}, 'B2':2}, 'A3':5}). У вас есть пример, который не делает то, что вы хотите?
Bscan
Зачем тестировать if isinstance(d, collections.Mapping)на каждой итерации? Смотри мой ответ .
Жером
13

То же решение, что и принятое, но более четкое именование переменных, строка документации и исправлена ​​ошибка, при которой {}значение не переопределялось.

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.

charlax
источник
7

Вот неизменяемая версия рекурсивного слияния словаря на случай, если это кому-нибудь понадобится.

Основано на ответе @Alex Martelli .

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
kabirbaidhya
источник
6

Незначительные улучшения в ответе @ 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
варочные панели
источник
1
Спасибо за это! К какому варианту использования может применяться параметр глубины?
Мэтт
@Matt, когда у вас есть объекты / диктовки на известной глубине, которые вы не хотите объединять / обновлять, просто перезаписываете новыми объектами (например, заменяете dict строкой или плавающей точкой или чем-то еще, глубоко в вашем dict)
hobs
1
Это работает, только если обновление не более чем на 1 уровень глубже, чем оригинал. Например, это терпит неудачу: update({'k1': 1}, {'k1': {'k2': {'k3': 3}}})я добавил ответ, который обращается к этому
bscan
@bscan хороший улов! никогда не думал об этом случае использования. Я полагаю, что мне следует глубже проникнуть в ветви элифов. Любые идеи?
варенье
Зачем тестировать if isinstance(d, Mapping)на каждой итерации? Смотри мой ответ . (Кроме того, я не уверен в вашем d = {k: u[k]})
Жером
4

Этот вопрос старый, но я попал сюда при поиске решения "глубокого слияния". Ответы выше вдохновили то, что следует. Я написал свою собственную, потому что во всех версиях, которые я тестировал, были ошибки. Пропущенная критическая точка состояла в том, что на некоторой произвольной глубине двух входных диктов для некоторого ключа 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))
djpinne
источник
4

Просто используйте 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

Фабио Каккамо
источник
2

Ни в одном из этих ответов авторы, кажется, не понимают концепцию обновления объекта, хранящегося в словаре, и даже итерации по элементам словаря (в отличие от ключей). Так что мне пришлось написать такой, который не делал бы бессмысленные тавтологические словари для хранения и поиска. Предполагается, что дикты хранят другие диктанты или простые типы.

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
PANDA-34
источник
2

Обновите ответ @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.

thuzhf
источник
2

Я использовал решение, которое предлагает @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
Helvete
источник
2

Код ниже должен решить update({'k1': 1}, {'k1': {'k2': 2}})проблему в ответе @Alex Martelli правильным способом.

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
Жером
источник
1
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

honmaple
источник
1

Я знаю, что этот вопрос довольно старый, но я все еще публикую сообщения о том, что я делаю, когда мне нужно обновить вложенный словарь. Мы можем использовать тот факт, что dicth передаются по ссылке в 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")

Это обновит словарь правильно.

ipsuri
источник
1

Возможно, вы наткнулись на нестандартный словарь, такой как я сегодня, у которого нет атрибута iteritems. В этом случае этот тип словаря легко интерпретировать как стандартный словарь. Например: 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()
noragen
источник
0

Да! И еще одно решение. Мое решение отличается ключами, которые проверяются. Во всех других решениях мы рассматриваем только ключи 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
zwep
источник
0

Если вы хотите заменить «полный вложенный словарь массивами», вы можете использовать этот фрагмент:

Он заменит любое «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'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
ZettaCircl
источник
0

Еще один способ использования рекурсии:

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)
yifyan
источник
0

новый вопрос как к цепочке ключей

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}}
user7337353
источник
0

Вы можете попробовать это, это работает со списками и чисто:

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
Крейг Н.
источник
0

Я рекомендую заменить {}на type(v)(), чтобы распространить тип объекта любого подкласса dict, хранящегося, uно отсутствующего в d. Например, это сохранит типы, такие как collection.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
Нико
источник
-1

Это немного в стороне, но вам действительно нужны вложенные словари? В зависимости от проблемы, иногда плоского словаря может быть достаточно ... и хорошо выглядеть:

>>> 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}
Нас Банов
источник
5
Вложенная структура происходит из входящих наборов данных json, поэтому я хотел бы сохранить их нетронутыми,
jay_t
-1

Если вы хотите однострочник:

{**dictionary1, **{'level1':{**dictionary1['level1'], **{'level2':{**dictionary1['level1']['level2'], **{'levelB':10}}}}}}
Джо'
источник