Обновить значение вложенного словаря различной глубины - PullRequest
135 голосов
/ 13 июля 2010

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

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

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

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

Ответы [ 16 ]

228 голосов
/ 13 июля 2010

@ Ответ 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 лучше всего применять для абстрактных базовых классов (не конкретных) для общности.

20 голосов
/ 23 августа 2013

Взял меня немного на этот, но благодаря посту @ Алекса, он заполнил пробел, который я пропустил. Однако я столкнулся с проблемой, если значение в рекурсивном 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
15 голосов
/ 02 сентября 2015

@ Алекс ответит хорошо, но не работает при замене элемента, такого как целое число, словарем, например 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}}})
9 голосов
/ 05 июня 2015

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

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.

6 голосов
/ 27 декабря 2012

Незначительные улучшения @ Alex's 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
5 голосов
/ 05 апреля 2017

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

Основано на ответе @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
3 голосов
/ 30 августа 2018

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

Я использовал решение, предложенное Алексом Мартелли, но оно не работает

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 голосов
/ 29 июня 2016

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

2 голосов
/ 29 февраля 2016

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

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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...