Оновіть значення вкладеного словника різної глибини


162

Я шукаю спосіб оновити словник dict1 зі змістом оновлення dict без перезапису рівняA

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

Я знаю, що оновлення видаляє значення в level2, оскільки воно оновлює найнижчий ключовий рівень1.

Як я міг би вирішити це, враховуючи, що словник1 та оновлення можуть мати будь-яку довжину?


Чи завжди гніздування три рівня глибоке чи ви можете вкладати довільну глибину?
ChristopheD

Він може мати будь-яку глибину / довжину.
jay_t

Виправте мене, якщо я помиляюся, але здається, що ідеальне рішення тут вимагає впровадження складеної схеми дизайну.
Олександр

Відповіді:


263

@ FM відповідь має правильне загальне уявлення, тобто рекурсивне рішення, але дещо своєрідне кодування і хоча б одну помилку. Я замість цього рекомендую:

Пітон 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

Пітон 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]}угоду про випадок, коли дікт оновлення глибший за вихідний. Рада редагувати відповідь, але не хочете забруднити стислий код, який вирішує проблему ОП.
варильні панелі

1
Навіщо використовувати, isinstance(v, collections.Mapping)а не isinstance(v, dict)? У випадку, якщо ОП вирішить розпочати використання колекцій?
Метт

2
@Matt Yea або будь-який інший похідний об'єкт (списки пар речей). Робить цю функцію більш загальною і менше шансів спокійно ігнорувати похідні об’єкти відображення та залишати їх не оновленими (підступна помилка, яку ОП може ніколи не бачити / ловити). Ви майже завжди хочете використовувати Mapping для пошуку типів dict та basestring для пошуку типів str.
варильні панелі

2
Якщо ви запускаєте цю програму під зміною Python 3+ u.iteritems()на u.items(), в іншому випадку ви зіткнетеся:AttributeError: 'dict' object has no attribute 'iteritems'
Грег K

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
Оскільки дикти є змінними, ви змінюєте екземпляр, який ви передаєте як аргумент. Тоді вам не потрібно повертати orig_dict.
gabrielhpugliese

3
Я думаю, що більшість людей очікує, що визначення поверне оновлений диктант, навіть якщо він оновлений на місці.
Кел Солаар

За замовчуванням у коді онсенді додається оновлений список до початкового списку. Якщо вам потрібно оновити перезаписати початковий список, вам потрібно встановити orig_dict [key] = val
intijk

1
@gabrielhpugliese повернення оригіналу потрібно, якщо його називають словниковим merged_tree = update({'default': {'initialvalue': 1}}, other_tree)
словом

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}}})

Я бачу. Ви зробили мою elifперевірку типу оригінального об'єкта "огороджувальним" умовним, що містить перевірки як значення, так і ключа цього дикта / карти. Розумний.
варильні панелі

Це не спрацює, якщо внутрішній дикт має більше одного ключа.
Wlerin

@Wlerin, він все ще працює; d до цього моменту стане Mapping. Ось тест з декількома ключами: update({'A1': 1, 'A2':2}, {'A1': {'B1': {'C1': 3, 'C2':4}, 'B2':2}, 'A3':5}). Чи є у вас приклад, який не робить те, що ви хочете?
bscan

Навіщо тестувати if isinstance(d, collections.Mapping)на ітерацію кожного з них? Дивіться мою відповідь .
Jérôme

13

Таке ж рішення, що і прийняте, але чіткіше іменування змінної, 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.


7

Ось незмінна версія рекурсивного словника злиття у випадку, якщо комусь це потрібно.

На основі @Alex Мартеллі в відповідь .

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

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, коли у вас є деякі об'єкти / дикти на відомій глибині, яку ви не бажаєте об'єднати / оновити, просто перезапись новими об’єктами (наприклад, заміною дикту на рядок або float або будь-яким іншим, глибоко у вашому диктаті)
варильні майданчики

1
Це працює лише в тому випадку, якщо оновлення знаходиться на максимум на 1 рівень глибше оригіналу. Наприклад, це не вдається: update({'k1': 1}, {'k1': {'k2': {'k3': 3}}})я додав відповідь, яка вирішує цю
проблему

@bscan гарний улов! ніколи не замислювався над цим випадком використання. Я думаю, мені слід повторитись глибше в гілках еліфу. Будь-які ідеї?
варильні панелі

Навіщо тестувати if isinstance(d, Mapping)на ітерацію кожного з них? Дивіться мою відповідь . (Також я не впевнений у вашому d = {k: u[k]})
Jérôme

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))

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

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_factorys.

Також зверніть увагу , що u.iteritems()було змінено u.items()в Python3.


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

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


1

Я знаю, що це питання досить старе, але все ще публікую те, що я роблю, коли мені потрібно оновити вкладений словник. Ми можемо використати той факт, що дикти передаються посиланням у python Припускаючи, що шлях ключа відомий і розділений крапкою. Форекс, якщо у нас є дакт з назвою даних:

{
"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")

Це б оновило словник правильно.


1

Можливо, ви натрапите на нестандартний словник, як я сьогодні, який не має атрибутів iteritems-Attribute. У цьому випадку легко тлумачити цей тип словника як стандартний словник. Напр .: 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()

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

0

Якщо ви хочете замінити "повний вкладений словник з масивами", ви можете використовувати цей фрагмент:

Він замінить будь-яке "old_value" на "new_value". Приблизно відбувається глибока спочатку перебудова словника. Він навіть може працювати зі списком або 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

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)

0

новий Q як до ланцюжків ключів

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}}

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. Наприклад, це зберегло б такі типи, як колекції.OrрадDict:

Пітон 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

Пітон 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}}}}}}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.