Atualizar valor de um dicionário aninhado de profundidade variável


162

Estou procurando uma maneira de atualizar o dict dictionary1 com o conteúdo da atualização do dict sem substituir o nívelA

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

Eu sei que a atualização exclui os valores no nível2 porque está atualizando o nível de chave mais baixo1.

Como eu poderia resolver isso, considerando que o dictionary1 e a atualização podem ter algum comprimento?


O aninhamento sempre tem três níveis de profundidade ou você pode aninhar uma profundidade arbitrária?
ChristopheD

Pode ter qualquer profundidade / comprimento.
21138 jay_t

Corrija-me se eu estiver errado, mas parece que a solução ideal aqui requer a implementação do padrão de design composto.
Alexander McNulty

Respostas:


263

A resposta do @ FM tem a idéia geral correta, ou seja, uma solução recursiva, mas uma codificação um tanto peculiar e pelo menos um bug. Eu recomendo:

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

Os shows de bugs-se quando o "update" tem um k, vartigo onde vé um dicte knão é originalmente uma chave no dicionário sendo atualizado - @ "salta" da FM código esta parte da atualização (porque ele executa-lo em um novo vazio dictque não é salvo ou retornado a lugar algum, apenas perdido quando a chamada recursiva retorna).

Minhas outras alterações são menores: não há razão para a construção if/ elsequando .getfaz o mesmo trabalho de maneira mais rápida e limpa, e isinstanceé melhor aplicada a classes base abstratas (não concretas) por generalidade.


7
+1 Boa captura do bug - doh! Imaginei que alguém teria uma maneira melhor de lidar com o isinstanceteste, mas pensei em dar uma facada nele.
FMc 13/07/10

6
Outro "recurso" menor faz com que isso aumente TypeError: 'int' object does not support item assignment.quando você, por exemplo update({'k1': 1}, {'k1': {'k2': 2}}). Para alterar esse comportamento e, em vez disso, expanda a profundidade dos dicionários para liberar espaço para dicionários mais profundos, você pode adicionar um elif isinstance(d, Mapping):ao redor d[k] = u[k]da isinstancecondição e depois dela. Você também precisará adicionar um else: d = {k: u[k]}para lidar com o caso em que o ditado de atualização é mais profundo que o ditado original. É um prazer editar a resposta, mas não deseja um código conciso sujo que resolva o problema do OP.
Placas

1
Por que usar em isinstance(v, collections.Mapping)vez de isinstance(v, dict)? Caso o OP decida começar a usar coleções?
Matt

2
@ Matt Yea ou qualquer outro objeto derivado de mapeamento (lista de pares de coisas). Torna a função mais geral e com menor probabilidade de ignorar silenciosamente os objetos derivados do mapeamento e os deixa sem atualização (erro insidioso que o OP pode nunca ver / capturar). Você quase sempre deseja usar o Mapeamento para encontrar tipos de ditado e a base para encontrar os tipos str.
Placas,

2
Se você estiver executando isso no Python 3+, altere u.iteritems()para u.items(), caso contrário, você encontrará:AttributeError: 'dict' object has no attribute 'iteritems'
Greg K

23

Levei-me um pouco neste caso, mas graças ao post de Alex, ele preencheu a lacuna que estava faltando. No entanto, me deparei com um problema se um valor dentro do recursivo dictfor um list, então pensei em compartilhar e estender sua resposta.

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
Eu acho que isso provavelmente deve ser (para ser um pouco mais seguro): orig_dict.get(key, []) + val.
Andy Hayden

2
Como os ditados são mutáveis, você está alterando a instância que está passando como argumento. Então, você não precisa retornar orig_dict.
gabrielhpugliese

3
Eu acho que a maioria das pessoas espera que a definição retorne o ditado atualizado, mesmo que seja atualizado no local.
precisa

A lógica padrão no código do onosendi é anexar a lista atualizada à lista original. Se você precisar atualizar, substitua a lista original, defina orig_dict [key] = val
intijk

1
@gabrielhpugliese retornar o original é necessário se chamado com um literal dicionário, por exemplo merged_tree = update({'default': {'initialvalue': 1}}, other_tree)
EoghanM

18

@ A resposta de Alex é boa, mas não funciona ao substituir um elemento como um número inteiro por um dicionário, como update({'foo':0},{'foo':{'bar':1}}). Esta atualização aborda:

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

Entendo. Você fez minha elifverificação do tipo de objeto original como um condicional "fechado", contendo as verificações do valor e da chave desse ditado / mapeamento. Inteligente.
hobs

Isso não funcionará se o ditado interno tiver mais de uma chave.
Wlerin

@ Wlerin, ainda funciona; d terá se tornado um mapeamento nesse ponto. Aqui está um caso de teste com várias chaves: update({'A1': 1, 'A2':2}, {'A1': {'B1': {'C1': 3, 'C2':4}, 'B2':2}, 'A3':5}). Você tem um exemplo que não faz o que você quer?
bscan 27/02

Por que testar if isinstance(d, collections.Mapping)em todas as iterações? Veja minha resposta .
Jérôme

13

A mesma solução que a aceita, mas a nomenclatura das variáveis ​​mais clara, a sequência de caracteres e corrigiu um erro que, {}como valor, não seria substituído.

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

Aqui estão alguns casos de teste:

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

Esta função está disponível no pacote charlatan , em charlatan.utils.


7

Aqui está uma versão imutável da mesclagem recursiva de dicionário, caso alguém precise.

Com base na resposta de @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

6

Pequenas melhorias na resposta do @ Alex, que permitem a atualização de dicionários de diferentes profundidades, além de limitar a profundidade que a atualização mergulha no dicionário aninhado original (mas a profundidade do dicionário de atualização não é limitada). Apenas alguns casos foram testados:

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
Obrigado por isso! Em que caso de uso o parâmetro depth pode ser aplicado?
Matt

@ Mat quando você tem alguns objetos / dictos em uma profundidade conhecida que não deseja mesclar / atualizar, apenas sobrescritos por novos objetos (como substituir um dict por uma string ou float ou qualquer outra coisa, no fundo do dict)
hobs

1
Isso só funciona se a atualização for no máximo 1 nível mais profunda que a original. Por exemplo, esta falha: update({'k1': 1}, {'k1': {'k2': {'k3': 3}}})Eu adicionei uma resposta que aborda esta
bscan

@bscan boa captura! nunca pensei nesse caso de uso. Acho que devo recuar mais profundamente nos ramos elif. Alguma ideia?
hobs

Por que testar if isinstance(d, Mapping)em todas as iterações? Veja minha resposta . (Também não tenho certeza do seu d = {k: u[k]})
Jérôme

4

Essa pergunta é antiga, mas eu cheguei aqui ao procurar uma solução de "mesclagem profunda". As respostas acima inspiraram o que se segue. Acabei escrevendo o meu próprio porque havia bugs em todas as versões que testei. O ponto crítico perdido foi, em alguma profundidade arbitrária dos dois ditados de entrada, para algumas chaves, k, a árvore de decisão quando d [k] ou u [k] não é um ditado com defeito.

Além disso, esta solução não requer recursão, que é mais simétrica com a forma como dict.update()funciona e retorna 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

Basta usar python-benedict (eu fiz) , ele tem um mergemétodo utilitário (deepupdate) e muitos outros. Funciona com python 2 / python 3 e é bem testado.

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

Instalação: pip install python-benedict

Documentação: https://github.com/fabiocaccamo/python-benedict


2

Em nenhuma dessas respostas, os autores parecem entender o conceito de atualizar um objeto armazenado em um dicionário, nem mesmo de iterar sobre os itens do dicionário (em oposição às chaves). Então, eu tive que escrever um que não faça armazenamentos e recuperações inúteis de dicionário tautológico. Presume-se que os dictos armazenem outros dictos ou tipos simples.

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

Ou ainda mais simples, trabalhando com qualquer tipo:

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

Atualize a resposta de @Alex Martelli para corrigir um erro em seu código para tornar a solução mais robusta:

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

A chave é que geralmente queremos criar o mesmo tipo na recursão; portanto, usamos aqui, v.copy().clear()mas não {}. E isso é especialmente útil se o dictaqui for do tipo collections.defaultdictque pode ter diferentes tipos de default_factorys.

Observe também que o u.iteritems()foi alterado para u.items()in Python3.


2

Usei a solução sugerida por Alex Martelli, mas ela falha

TypeError 'bool' object does not support item assignment

quando os dois dicionários diferem no tipo de dados em algum nível.

No mesmo nível, o elemento do dicionário dé apenas um escalar (isto é, Bool) enquanto o elemento do dicionário uainda é o dicionário, a reatribuição falha, pois nenhuma atribuição de dicionário é possível no escalar (como True[k]).

Uma condição adicionada corrige que:

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

O código abaixo deve resolver o update({'k1': 1}, {'k1': {'k2': 2}})problema da resposta de @Alex Martelli da maneira certa.

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

use dictoucollections.Mapping


1

Sei que essa pergunta é bastante antiga, mas ainda estou postando o que faço quando preciso atualizar um dicionário aninhado. Podemos usar o fato de que os dict são passados ​​por referência em python Supondo que o caminho da chave seja conhecido e seja separado por pontos. Forex se tivermos um ditado chamado data:

{
"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/"
}

E queremos atualizar a classe da fila, o caminho da chave seria - log_config_worker.handlers.queue.class

Podemos usar a seguinte função para atualizar o valor:

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

Isso atualizaria o dicionário corretamente.


1

Pode ser que você tropeça em um dicionário não-padrão, como eu hoje, que não possui iteritems-Attribute. Nesse caso, é fácil interpretar esse tipo de dicionário como um dicionário padrão. Por exemplo: 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

Sim! E outra solução. Minha solução difere nas chaves que estão sendo verificadas. Em todas as outras soluções, apenas analisamos as chaves dict_b. Mas aqui olhamos para a união dos dois dicionários.

Faça como quiser

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

Se você deseja substituir um "dicionário aninhado completo por matrizes", pode usar este snippet:

Ele substituirá qualquer "valor antigo" por "novo valor". Ele está fazendo uma reconstrução profunda e profunda do dicionário. Pode até funcionar com List ou Str / int, dado como parâmetro de entrada do primeiro nível.

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

Outra maneira de usar a recursão:

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

um novo Q como Por uma corrente de chaves

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

você pode tentar isso, ele funciona com listas e é puro:

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

Eu recomendo substituir {}por type(v)()para propagar o tipo de objeto de qualquer subclasse de dict armazenada, umas ausente d. Por exemplo, isso preservaria tipos como coleções.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

Isso é um pouco demais, mas você realmente precisa de dicionários aninhados? Dependendo do problema, às vezes um dicionário plano pode ser suficiente ... e ter uma boa aparência:

>>> 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
A estrutura aninhada vem de conjuntos de dados JSON de entrada, então eu gostaria de mantê-las intactas, ...
jay_t

-1

Se você deseja uma linha:

{**dictionary1, **{'level1':{**dictionary1['level1'], **{'level2':{**dictionary1['level1']['level2'], **{'levelB':10}}}}}}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.