Acessar itens de dicionário aninhados por meio de uma lista de chaves?

143

Eu tenho uma estrutura de dicionário complexa que gostaria de acessar por meio de uma lista de chaves para abordar o item correto.

dataDict = {
    "a":{
        "r": 1,
        "s": 2,
        "t": 3
        },
    "b":{
        "u": 1,
        "v": {
            "x": 1,
            "y": 2,
            "z": 3
        },
        "w": 3
        }
}    

maplist = ["a", "r"]

ou

maplist = ["b", "v", "y"]

Eu criei o seguinte código que funciona, mas tenho certeza de que existe uma maneira melhor e mais eficiente de fazer isso, se alguém tiver uma ideia.

# Get a given data from a dictionary with position provided as a list
def getFromDict(dataDict, mapList):    
    for k in mapList: dataDict = dataDict[k]
    return dataDict

# Set a given data in a dictionary with position provided as a list
def setInDict(dataDict, mapList, value): 
    for k in mapList[:-1]: dataDict = dataDict[k]
    dataDict[mapList[-1]] = value
Kolergy
fonte

Respostas:

230

Use reduce()para percorrer o dicionário:

from functools import reduce  # forward compatibility for Python 3
import operator

def getFromDict(dataDict, mapList):
    return reduce(operator.getitem, mapList, dataDict)

e reutilize getFromDictpara encontrar o local para armazenar o valor para setInDict():

def setInDict(dataDict, mapList, value):
    getFromDict(dataDict, mapList[:-1])[mapList[-1]] = value

Todos os itens, exceto o último elemento, mapListsão necessários para encontrar o dicionário 'pai' ao qual adicionar o valor e, em seguida, use o último elemento para definir o valor na chave correta.

Demo:

>>> getFromDict(dataDict, ["a", "r"])
1
>>> getFromDict(dataDict, ["b", "v", "y"])
2
>>> setInDict(dataDict, ["b", "v", "w"], 4)
>>> import pprint
>>> pprint.pprint(dataDict)
{'a': {'r': 1, 's': 2, 't': 3},
 'b': {'u': 1, 'v': {'w': 4, 'x': 1, 'y': 2, 'z': 3}, 'w': 3}}

Observe que o guia de estilo do Python PEP8 prescreve nomes de snake_case para funções . O exemplo acima funciona igualmente bem para listas ou uma mistura de dicionários e listas; portanto, os nomes devem realmente ser get_by_path()e set_by_path():

from functools import reduce  # forward compatibility for Python 3
import operator

def get_by_path(root, items):
    """Access a nested object in root by item sequence."""
    return reduce(operator.getitem, items, root)

def set_by_path(root, items, value):
    """Set a value in a nested object in root by item sequence."""
    get_by_path(root, items[:-1])[items[-1]] = value
Martijn Pieters
fonte
1
Quanto essa travessia é confiável para estruturas aninhadas arbitrárias? Também funcionará para dicionários mistos com listas aninhadas? Como modifico getFromDict () para fornecer o valor padrão e ter o valor padrão como Nenhum? Sou iniciante em Python com muitos anos de desenvolvimento PHP e antes do desenvolvimento em C.
Dmitriy Sintsov
2
O conjunto mapeado aninhado também deve criar nós inexistentes, imo: lists para chaves inteiras, dicionários para chaves de cadeia.
Dmitriy Sintsov
1
@ user1353510: por acaso, a sintaxe de indexação regular é usada aqui, então também suporta listas dentro de dicionários. Basta passar índices inteiros para eles.
Martijn Pieters
1
@ user1353510: para um valor padrão, o uso try:, except (KeyError, IndexError): return default_valueem torno da atual returnlinha.
Martijn Pieters
1
@ Georgy: o uso dict.get()altera a semântica, pois ela retorna em Nonevez de aumentar KeyErrorpor nomes ausentes. Quaisquer nomes subseqüentes acionam um AttributeError. operatoré uma biblioteca padrão, não há necessidade de evitá-la aqui.
Martijn Pieters
40
  1. A solução aceita não funcionará diretamente para python3 - precisará de um from functools import reduce.
  2. Também parece mais pitônico usar um forloop. Veja a citação do What's New In Python 3.0 .

    Removido reduce() . Use functools.reduce()se você realmente precisar; no entanto, 99% das vezes que um forloop explícito é mais legível.

  3. Em seguida, a solução aceita não define chaves aninhadas inexistentes (retorna a KeyError) - consulte a resposta da @ eafit para obter uma solução

Então, por que não usar o método sugerido da pergunta de kolergy para obter um valor:

def getFromDict(dataDict, mapList):    
    for k in mapList: dataDict = dataDict[k]
    return dataDict

E o código da resposta da @ eafit para definir um valor:

def nested_set(dic, keys, value):
    for key in keys[:-1]:
        dic = dic.setdefault(key, {})
    dic[keys[-1]] = value

Ambos funcionam diretamente no python 2 e 3

DomTomCat
fonte
6
Eu prefiro esta solução - mas tenha cuidado. Se não me engano, como os dicionários Python não são imutáveis getFromDict, podem destruir o chamador dataDict. Eu faria copy.deepcopy(dataDict)primeiro. Obviamente, (como está escrito), esse comportamento é desejado na segunda função.
Dylan F
15

Usar reduzir é inteligente, mas o método de conjunto do OP pode ter problemas se as chaves pai não existirem no dicionário aninhado. Como este é o primeiro post do SO que vi sobre esse assunto na minha pesquisa no google, gostaria de torná-lo um pouco melhor.

O método set em ( Definindo um valor em um dicionário python aninhado, dada uma lista de índices e valores ) parece mais robusto à falta de chaves parentais. Para copiá-lo:

def nested_set(dic, keys, value):
    for key in keys[:-1]:
        dic = dic.setdefault(key, {})
    dic[keys[-1]] = value

Além disso, pode ser conveniente ter um método que percorra a árvore de chaves e obtenha todos os caminhos de chave absolutos, para os quais eu criei:

def keysInDict(dataDict, parent=[]):
    if not isinstance(dataDict, dict):
        return [tuple(parent)]
    else:
        return reduce(list.__add__, 
            [keysInDict(v,parent+[k]) for k,v in dataDict.items()], [])

Um uso disso é converter a árvore aninhada em um DataFrame do pandas, usando o código a seguir (assumindo que todas as folhas do dicionário aninhado tenham a mesma profundidade).

def dict_to_df(dataDict):
    ret = []
    for k in keysInDict(dataDict):
        v = np.array( getFromDict(dataDict, k), )
        v = pd.DataFrame(v)
        v.columns = pd.MultiIndex.from_product(list(k) + [v.columns])
        ret.append(v)
    return reduce(pd.DataFrame.join, ret)
eafit
fonte
por que arbitrariamente limitar o comprimento do argumento 'keys' a 2 ou mais in nested_set?
Alancalvitti
10

Esta biblioteca pode ser útil: https://github.com/akesterson/dpath-python

Uma biblioteca python para acessar e pesquisar dicionários via / slashed / caminhos ala xpath

Basicamente, ele permite que você navegue por um dicionário como se fosse um sistema de arquivos.

dmmfll
fonte
3

Que tal usar funções recursivas?

Para obter um valor:

def getFromDict(dataDict, maplist):
    first, rest = maplist[0], maplist[1:]

    if rest: 
        # if `rest` is not empty, run the function recursively
        return getFromDict(dataDict[first], rest)
    else:
        return dataDict[first]

E para definir um valor:

def setInDict(dataDict, maplist, value):
    first, rest = maplist[0], maplist[1:]

    if rest:
        try:
            if not isinstance(dataDict[first], dict):
                # if the key is not a dict, then make it a dict
                dataDict[first] = {}
        except KeyError:
            # if key doesn't exist, create one
            dataDict[first] = {}

        setInDict(dataDict[first], rest, value)
    else:
        dataDict[first] = value
xyres
fonte
2

Estilo Python puro, sem qualquer importação:

def nested_set(element, value, *keys):
    if type(element) is not dict:
        raise AttributeError('nested_set() expects dict as first argument.')
    if len(keys) < 2:
        raise AttributeError('nested_set() expects at least three arguments, not enough given.')

    _keys = keys[:-1]
    _element = element
    for key in _keys:
        _element = _element[key]
    _element[keys[-1]] = value

example = {"foo": { "bar": { "baz": "ok" } } }
keys = ['foo', 'bar']
nested_set(example, "yay", *keys)
print(example)

Resultado

{'foo': {'bar': 'yay'}}
Arount
fonte
2

Uma maneira alternativa, se você não deseja gerar erros, se uma das chaves estiver ausente (para que seu código principal possa ser executado sem interrupção):

def get_value(self,your_dict,*keys):
    curr_dict_ = your_dict
    for k in keys:
        v = curr_dict.get(k,None)
        if v is None:
            break
        if isinstance(v,dict):
            curr_dict = v
    return v

Nesse caso, se alguma das teclas de entrada não estiver presente, Nenhuma será retornada, o que pode ser usado como uma verificação no código principal para executar uma tarefa alternativa.

Pulkit
fonte
1

Em vez de ter um desempenho atingido toda vez que você deseja procurar um valor, que tal achatar o dicionário uma vez e simplesmente procurar a chave como b:v:y

def flatten(mydict):
  new_dict = {}
  for key,value in mydict.items():
    if type(value) == dict:
      _dict = {':'.join([key, _key]):_value for _key, _value in flatten(value).items()}
      new_dict.update(_dict)
    else:
      new_dict[key]=value
  return new_dict

dataDict = {
"a":{
    "r": 1,
    "s": 2,
    "t": 3
    },
"b":{
    "u": 1,
    "v": {
        "x": 1,
        "y": 2,
        "z": 3
    },
    "w": 3
    }
}    

flat_dict = flatten(dataDict)
print flat_dict
{'b:w': 3, 'b:u': 1, 'b:v:y': 2, 'b:v:x': 1, 'b:v:z': 3, 'a:r': 1, 'a:s': 2, 'a:t': 3}

Dessa forma, você pode simplesmente procurar itens usando o flat_dict['b:v:y']que lhe dará1 .

E, em vez de percorrer o dicionário em cada pesquisa, você pode acelerar isso achatando o dicionário e salvando a saída, para que uma pesquisa a partir do início a frio signifique carregar o dicionário achatado e simplesmente realizar uma pesquisa de chave / valor sem Travessia.

OkezieE
fonte
1

Resolvido isso com recursão:

def get(d,l):
    if len(l)==1: return d[l[0]]
    return get(d[l[0]],l[1:])

Usando seu exemplo:

dataDict = {
    "a":{
        "r": 1,
        "s": 2,
        "t": 3
        },
    "b":{
        "u": 1,
        "v": {
            "x": 1,
            "y": 2,
            "z": 3
        },
        "w": 3
        }
}
maplist1 = ["a", "r"]
maplist2 = ["b", "v", "y"]
print(get(dataDict, maplist1)) # 1
print(get(dataDict, maplist2)) # 2
Poh Zi How
fonte
1

Que tal verificar e definir o elemento dict sem processar todos os índices duas vezes?

Solução:

def nested_yield(nested, keys_list):
    """
    Get current nested data by send(None) method. Allows change it to Value by calling send(Value) next time
    :param nested: list or dict of lists or dicts
    :param keys_list: list of indexes/keys
    """
    if not len(keys_list):  # assign to 1st level list
        if isinstance(nested, list):
            while True:
                nested[:] = yield nested
        else:
            raise IndexError('Only lists can take element without key')


    last_key = keys_list.pop()
    for key in keys_list:
        nested = nested[key]

    while True:
        try:
            nested[last_key] = yield nested[last_key]
        except IndexError as e:
            print('no index {} in {}'.format(last_key, nested))
            yield None

Exemplo de fluxo de trabalho:

ny = nested_yield(nested_dict, nested_address)
data_element = ny.send(None)
if data_element:
    # process element
    ...
else:
    # extend/update nested data
    ny.send(new_data_element)
    ...
ny.close()

Teste

>>> cfg= {'Options': [[1,[0]],[2,[4,[8,16]]],[3,[9]]]}
    ny = nested_yield(cfg, ['Options',1,1,1])
    ny.send(None)
[8, 16]
>>> ny.send('Hello!')
'Hello!'
>>> cfg
{'Options': [[1, [0]], [2, [4, 'Hello!']], [3, [9]]]}
>>> ny.close()
And0k
fonte
1

Muito tarde para a festa, mas publicar caso isso possa ajudar alguém no futuro. Para o meu caso de uso, a seguinte função funcionou melhor. Trabalha para extrair qualquer tipo de dados do dicionário

dict é o dicionário que contém nosso valor

list é uma lista de "etapas" em direção ao nosso valor

def getnestedvalue(dict, list):

    length = len(list)
    try:
        for depth, key in enumerate(list):
            if depth == length - 1:
                output = dict[key]
                return output
            dict = dict[key]
    except (KeyError, TypeError):
        return None

    return None
Jack Casey
fonte
1

É satisfatório ver essas respostas por ter dois métodos estáticos para definir e obter atributos aninhados. Essas soluções são muito melhores do que usar árvores aninhadas https://gist.github.com/hrldcpr/2012250

Aqui está a minha implementação.

Uso :

Para definir chamada de atributo aninhado sattr(my_dict, 1, 2, 3, 5) is equal to my_dict[1][2][3][4]=5

Para obter uma chamada de atributo aninhado gattr(my_dict, 1, 2)

def gattr(d, *attrs):
    """
    This method receives a dict and list of attributes to return the innermost value of the give dict       
    """
    try:
        for at in attrs:
            d = d[at]
        return d
    except(KeyError, TypeError):
        return None


def sattr(d, *attrs):
    """
    Adds "val" to dict in the hierarchy mentioned via *attrs
    For ex:
    sattr(animals, "cat", "leg","fingers", 4) is equivalent to animals["cat"]["leg"]["fingers"]=4
    This method creates necessary objects until it reaches the final depth
    This behaviour is also known as autovivification and plenty of implementation are around
    This implementation addresses the corner case of replacing existing primitives
    https://gist.github.com/hrldcpr/2012250#gistcomment-1779319
    """
    for attr in attrs[:-2]:
        if type(d.get(attr)) is not dict:
            d[attr] = {}
        d = d[attr]
    d[attrs[-2]] = attrs[-1]
nehem
fonte
1

Sugiro que você use python-benedictpara acessar itens aninhados usando o caminho da chave.

Instale-o usando pip:

pip install python-benedict

Então:

from benedict import benedict

dataDict = benedict({
    "a":{
        "r": 1,
        "s": 2,
        "t": 3,
    },
    "b":{
        "u": 1,
        "v": {
            "x": 1,
            "y": 2,
            "z": 3,
        },
        "w": 3,
    },
}) 

print(dataDict['a.r'])
# or
print(dataDict['a', 'r'])

Aqui está a documentação completa: https://github.com/fabiocaccamo/python-benedict

Fabio Caccamo
fonte
0

Se você também deseja trabalhar com json arbitrário, incluindo listas e dicts aninhados, e lidar bem com caminhos de pesquisa inválidos, aqui está minha solução:

from functools import reduce


def get_furthest(s, path):
    '''
    Gets the furthest value along a given key path in a subscriptable structure.

    subscriptable, list -> any
    :param s: the subscriptable structure to examine
    :param path: the lookup path to follow
    :return: a tuple of the value at the furthest valid key, and whether the full path is valid
    '''

    def step_key(acc, key):
        s = acc[0]
        if isinstance(s, str):
            return (s, False)
        try:
            return (s[key], acc[1])
        except LookupError:
            return (s, False)

    return reduce(step_key, path, (s, True))


def get_val(s, path):
    val, successful = get_furthest(s, path)
    if successful:
        return val
    else:
        raise LookupError('Invalid lookup path: {}'.format(path))


def set_val(s, path, value):
    get_val(s, path[:-1])[path[-1]] = value
Grant Palmer
fonte
0

um método para concatenar seqüências de caracteres:

def get_sub_object_from_path(dict_name, map_list):
    for i in map_list:
        _string = "['%s']" % i
        dict_name += _string
    value = eval(dict_name)
    return value
#Sample:
_dict = {'new': 'person', 'time': {'for': 'one'}}
map_list = ['time', 'for']
print get_sub_object_from_path("_dict",map_list)
#Output:
#one
lucas
fonte
0

Estendendo o @DomTomCat e a abordagem de outras pessoas, esses configuradores e mapeadores funcionais (ou seja, retornam dados modificados por meio de cópia em profundidade sem afetar a entrada) funcionam para o nested dictand list.

normatizador:

def set_at_path(data0, keys, value):
    data = deepcopy(data0)
    if len(keys)>1:
        if isinstance(data,dict):
            return {k:(set_by_path(v,keys[1:],value) if k==keys[0] else v) for k,v in data.items()}
        if isinstance(data,list):
            return [set_by_path(x[1],keys[1:],value) if x[0]==keys[0] else x[1] for x in enumerate(data)]
    else:
        data[keys[-1]]=value
        return data

mapeador:

def map_at_path(data0, keys, f):
    data = deepcopy(data0)
    if len(keys)>1:
        if isinstance(data,dict):
            return {k:(map_at_path(v,keys[1:],f) if k==keys[0] else v) for k,v in data.items()}
        if isinstance(data,list):
            return [map_at_path(x[1],keys[1:],f) if x[0]==keys[0] else x[1] for x in enumerate(data)]
    else:
        data[keys[-1]]=f(data[keys[-1]])
        return data
alancalvitti
fonte
0

Você pode fazer uso da evalfunção em python.

def nested_parse(nest, map_list):
    nestq = "nest['" + "']['".join(map_list) + "']"
    return eval(nestq, {'__builtins__':None}, {'nest':nest})

Explicação

Para sua consulta de exemplo: maplist = ["b", "v", "y"]

nestqserá "nest['b']['v']['y']"ondenest está o dicionário aninhado.

A evalfunção interna executa a sequência especificada. No entanto, é importante ter cuidado com as possíveis vulnerabilidades que surgem do uso da evalfunção. A discussão pode ser encontrada aqui:

  1. https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html
  2. https://www.journaldev.com/22504/python-eval-function

Na nested_parse()função, assegurei-me de que não houvesse __builtins__globais disponíveis e que apenas a variável local disponível fosse o nestdicionário.

Abhirup Das
fonte