Método seguro do Python para obter valor do dicionário aninhado

144

Eu tenho um dicionário aninhado. Existe apenas uma maneira de obter valores com segurança?

try:
    example_dict['key1']['key2']
except KeyError:
    pass

Ou talvez o python tenha um método semelhante get()ao dicionário aninhado?

Arti
fonte
1
O código na sua pergunta já é, na minha opinião, a melhor maneira de obter valores aninhados do dicionário. Você sempre pode especificar um valor padrão na except keyerror:cláusula.
Peter Schorn

Respostas:

280

Você pode usar getduas vezes:

example_dict.get('key1', {}).get('key2')

Isso retornará Nonese um key1oukey2 existir não.

Observe que isso ainda pode gerar um AttributeErrorif example_dict['key1']existe, mas não é um dict (ou um objeto do tipo dict com um getmétodo). O try..exceptcódigo que você postou aumentaria uma TypeErrorvez seexample_dict['key1'] ser subscrito.

Outra diferença é que os try...exceptcurtos-circuitos imediatamente após a primeira tecla ausente. A cadeia de getchamadas não.


Se você deseja preservar a sintaxe, example_dict['key1']['key2']mas não deseja que ele aumente KeyErrors, use a receita Hasher :

class Hasher(dict):
    # https://stackoverflow.com/a/3405143/190597
    def __missing__(self, key):
        value = self[key] = type(self)()
        return value

example_dict = Hasher()
print(example_dict['key1'])
# {}
print(example_dict['key1']['key2'])
# {}
print(type(example_dict['key1']['key2']))
# <class '__main__.Hasher'>

Observe que isso retorna um Hasher vazio quando uma chave está ausente.

Como Hasheré uma subclasse, dictvocê pode usar um Hasher da mesma maneira que você poderia usar um dict. Todos os mesmos métodos e sintaxe estão disponíveis, os Hashers tratam as chaves ausentes de maneira diferente.

Você pode converter um regular dictem um Hasherassim:

hasher = Hasher(example_dict)

e converta a Hasherpara regular com dicta mesma facilidade:

regular_dict = dict(hasher)

Outra alternativa é ocultar a feiura em uma função auxiliar:

def safeget(dct, *keys):
    for key in keys:
        try:
            dct = dct[key]
        except KeyError:
            return None
    return dct

Portanto, o restante do seu código pode ficar relativamente legível:

safeget(example_dict, 'key1', 'key2')
unutbu
fonte
37
então, python não tem uma solução bonita para esse caso? :( #
Arti
Encontrei um problema com uma implementação semelhante. Se você tiver d = {key1: None}, o primeiro get retornará None e, em seguida, você terá uma exceção): Estou tentando descobrir uma solução para isso
Huercio 31/01/18
1
De safegetmuitas maneiras, o método não é muito seguro, pois substitui o dicionário original, o que significa que você não pode fazer coisas como essas safeget(dct, 'a', 'b') or safeget(dct, 'a').
Neverfox
safegetnunca substitui o dicionário original. Ele retornará o dicionário original, um valor do dicionário original ou None.
Unutbu 12/0418
4
@KurtBourbaki: dct = dct[key] reatribui um novo valor à variável local dct . Isso não altera o ditado original (portanto, o ditado original não é afetado safeget.) Se, por outro lado, dct[key] = ...tivesse sido usado, o ditado original teria sido modificado. Em outras palavras, em Python, os nomes estão vinculados a valores . Atribuição de um novo valor para um nome não afeta o valor antigo (a menos que não há mais referências ao valor antigo, caso em que (em CPython) ele irá obter lixo coletado.)
unutbu
60

Você também pode usar o python reduzir :

def deep_get(dictionary, *keys):
    return reduce(lambda d, key: d.get(key) if d else None, keys, dictionary)
Yoav T
fonte
5
Só queria mencionar que o functools não é mais um componente do Python3 e precisa ser importado do functools, o que torna essa abordagem um pouco menos elegante.
YoniLavi
3
Ligeira correção para este comentário: reduzir não é mais um built-in no Py3. Mas não vejo por que isso torna isso menos elegante. Isso o torna menos adequado para uma linha, mas ser uma linha não qualifica ou desqualifica automaticamente algo como "elegante".
PaulMcG 24/01
30

Ao combinar todas essas respostas aqui e as pequenas alterações que fiz, acho que essa função seria útil. é seguro, rápido e de fácil manutenção.

def deep_get(dictionary, keys, default=None):
    return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)

Exemplo:

>>> from functools import reduce
>>> def deep_get(dictionary, keys, default=None):
...     return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)
...
>>> person = {'person':{'name':{'first':'John'}}}
>>> print (deep_get(person, "person.name.first"))
John
>>> print (deep_get(person, "person.name.lastname"))
None
>>> print (deep_get(person, "person.name.lastname", default="No lastname"))
No lastname
>>>
Yuda Prawira
fonte
1
Perfeito para modelos Jinja2
Thomas
Essa é uma boa solução, embora exista uma desvantagem: mesmo que a primeira chave não esteja disponível ou o valor passado como argumento do dicionário para a função não seja um dicionário, a função passará do primeiro elemento para o último. Basicamente, faz isso em todos os casos.
Arseny
1
deep_get({'a': 1}, "a.b")dá, Nonemas eu esperaria uma exceção como KeyErrorou algo mais.
stackunderflow
@edityouprofile. então você só precisa fazer pequenas modificações para alterar o valor de retorno de NoneparaRaise KeyError
Yuda Prawira
15

Com base na resposta de Yoav, uma abordagem ainda mais segura:

def deep_get(dictionary, *keys):
    return reduce(lambda d, key: d.get(key, None) if isinstance(d, dict) else None, keys, dictionary)
Jose Alban
fonte
12

Uma solução recursiva. Não é o mais eficiente, mas acho que é um pouco mais legível do que os outros exemplos e não depende de funções.

def deep_get(d, keys):
    if not keys or d is None:
        return d
    return deep_get(d.get(keys[0]), keys[1:])

Exemplo

d = {'meta': {'status': 'OK', 'status_code': 200}}
deep_get(d, ['meta', 'status_code'])     # => 200
deep_get(d, ['garbage', 'status_code'])  # => None

Uma versão mais polida

def deep_get(d, keys, default=None):
    """
    Example:
        d = {'meta': {'status': 'OK', 'status_code': 200}}
        deep_get(d, ['meta', 'status_code'])          # => 200
        deep_get(d, ['garbage', 'status_code'])       # => None
        deep_get(d, ['meta', 'garbage'], default='-') # => '-'
    """
    assert type(keys) is list
    if d is None:
        return default
    if not keys:
        return d
    return deep_get(d.get(keys[0]), keys[1:], default)
Pithikos
fonte
7

Embora a abordagem de redução seja organizada e curta, acho que um loop simples é mais fácil de criar. Eu também incluí um parâmetro padrão.

def deep_get(_dict, keys, default=None):
    for key in keys:
        if isinstance(_dict, dict):
            _dict = _dict.get(key, default)
        else:
            return default
    return _dict

Como um exercício para entender como a redução de uma linha funcionava, fiz o seguinte. Mas, em última análise, a abordagem do loop parece mais intuitiva para mim.

def deep_get(_dict, keys, default=None):

    def _reducer(d, key):
        if isinstance(d, dict):
            return d.get(key, default)
        return default

    return reduce(_reducer, keys, _dict)

Uso

nested = {'a': {'b': {'c': 42}}}

print deep_get(nested, ['a', 'b'])
print deep_get(nested, ['a', 'b', 'z', 'z'], default='missing')
zzz
fonte
5

Eu sugiro que você tente python-benedict .

É um dict subclasse que fornece suporte ao caminho-chave e muito mais.

Instalação: pip install python-benedict

from benedict import benedict

example_dict = benedict(example_dict, keypath_separator='.')

agora você pode acessar valores aninhados usando o caminho da chave :

val = example_dict['key1.key2']

# using 'get' method to avoid a possible KeyError:
val = example_dict.get('key1.key2')

ou acesse valores aninhados usando a lista de chaves :

val = example_dict['key1', 'key2']

# using get to avoid a possible KeyError:
val = example_dict.get(['key1', 'key2'])

É bem testado e de código aberto no GitHub :

https://github.com/fabiocaccamo/python-benedict

Fabio Caccamo
fonte
@ perfecto25 obrigado! Em breve lançarei novos recursos, fique atento Fab
Fabio Caccamo
@ perfecto25 Adicionei suporte para listar índices, por exemplo. d.get('a.b[0].c[-1]')
Fabio Caccamo 31/01
4

Uma classe simples que pode agrupar um ditado e recuperar com base em uma chave:

class FindKey(dict):
    def get(self, path, default=None):
        keys = path.split(".")
        val = None

        for key in keys:
            if val:
                if isinstance(val, list):
                    val = [v.get(key, default) if v else None for v in val]
                else:
                    val = val.get(key, default)
            else:
                val = dict.get(self, key, default)

            if not val:
                break

        return val

Por exemplo:

person = {'person':{'name':{'first':'John'}}}
FindDict(person).get('person.name.first') # == 'John'

Se a chave não existir, ela retornará Nonepor padrão. Você pode substituir isso usando uma default=chave no FindDictwrapper - por exemplo`:

FindDict(person, default='').get('person.name.last') # == doesn't exist, so ''
Lee Benson
fonte
3

para uma recuperação de chave de segundo nível, você pode fazer o seguinte:

key2_value = (example_dict.get('key1') or {}).get('key2')
Jacob CUI
fonte
2

Depois de ver isso para obter atributos profundamente, fiz o seguinte para obter dictvalores aninhados com segurança usando a notação de ponto. Isso funciona para mim porque meus dictsobjetos MongoDB são desserializados, então eu sei que os nomes das chaves não contêm .s. Além disso, no meu contexto, posso especificar um valor de fallback falso ( None) que não possuo nos meus dados, para evitar o padrão try / except ao chamar a função.

from functools import reduce # Python 3
def deepgetitem(obj, item, fallback=None):
    """Steps through an item chain to get the ultimate value.

    If ultimate value or path to value does not exist, does not raise
    an exception and instead returns `fallback`.

    >>> d = {'snl_final': {'about': {'_icsd': {'icsd_id': 1}}}}
    >>> deepgetitem(d, 'snl_final.about._icsd.icsd_id')
    1
    >>> deepgetitem(d, 'snl_final.about._sandbox.sbx_id')
    >>>
    """
    def getitem(obj, name):
        try:
            return obj[name]
        except (KeyError, TypeError):
            return fallback
    return reduce(getitem, item.split('.'), obj)
Donny Winston
fonte
7
fallbacknão é realmente usado na função
153957
Observe que isso não funciona para chaves que contêm um.
JW.
Quando chamamos obj [name], por que não obj.get (name, fallback) e evita o try-catch (se você quiser o try-catch, retorne fallback, não None)
denvar
Obrigado @ 153957. Eu consertei isso. E sim @JW, isso funciona para o meu caso de uso. Você pode adicionar uma sep=','palavra - chave arg para generalizar para determinadas condições (sep, fallback). E @denvar, se objé o tipo de letra intapós uma sequência de redução, então obj [name] gera um TypeError, que eu pego. Se eu usasse obj.get (name) ou obj.get (name, fallback), isso geraria um AttributeError, então, de qualquer maneira, eu precisaria pegar.
Donny Winston
1

Ainda outra função para a mesma coisa, também retorna um booleano para representar se a chave foi encontrada ou não e lida com alguns erros inesperados.

'''
json : json to extract value from if exists
path : details.detail.first_name
            empty path represents root

returns a tuple (boolean, object)
        boolean : True if path exists, otherwise False
        object : the object if path exists otherwise None

'''
def get_json_value_at_path(json, path=None, default=None):

    if not bool(path):
        return True, json
    if type(json) is not dict :
        raise ValueError(f'json={json}, path={path} not supported, json must be a dict')
    if type(path) is not str and type(path) is not list:
        raise ValueError(f'path format {path} not supported, path can be a list of strings like [x,y,z] or a string like x.y.z')

    if type(path) is str:
        path = path.strip('.').split('.')
    key = path[0]
    if key in json.keys():
        return get_json_value_at_path(json[key], path[1:], default)
    else:
        return False, default

exemplo de uso:

my_json = {'details' : {'first_name' : 'holla', 'last_name' : 'holla'}}
print(get_json_value_at_path(my_json, 'details.first_name', ''))
print(get_json_value_at_path(my_json, 'details.phone', ''))

(Verdadeiro, 'holla')

(Falso '')

penduDev
fonte
0

Uma adaptação da resposta de unutbu que achei útil em meu próprio código:

example_dict.setdefaut('key1', {}).get('key2')

Ele gera uma entrada de dicionário para key1 se ainda não tiver essa chave, para evitar o KeyError. Se você quiser acabar com um dicionário aninhado que inclua esse emparelhamento de teclas como eu fiz, essa parece ser a solução mais fácil.

GenesRus
fonte
0

Como gerar um erro de chave, se uma das chaves estiver faltando, é uma coisa razoável a se fazer, não podemos nem mesmo procurá-lo e torná-lo tão único quanto isso:

def get_dict(d, kl):
  cur = d[kl[0]]
  return get_dict(cur, kl[1:]) if len(kl) > 1 else cur
Ben Usman
fonte
0

Pouca melhoria na reduceabordagem para fazê-la funcionar com a lista. Também usando o caminho de dados como sequência dividida por pontos em vez de matriz.

def deep_get(dictionary, path):
    keys = path.split('.')
    return reduce(lambda d, key: d[int(key)] if isinstance(d, list) else d.get(key) if d else None, keys, dictionary)
Mike Kor
fonte
0

Uma solução que eu usei é semelhante ao double get, mas com a capacidade adicional de evitar um TypeError usando a lógica if else:

    value = example_dict['key1']['key2'] if example_dict.get('key1') and example_dict['key1'].get('key2') else default_value

No entanto, quanto mais aninhado o dicionário, mais complicado isso se torna.

Adam The Housman
fonte
0

Para pesquisas aninhadas de dicionário / JSON, você pode usar o dictor

dict de instalação do pip

objeto ditado

{
    "characters": {
        "Lonestar": {
            "id": 55923,
            "role": "renegade",
            "items": [
                "space winnebago",
                "leather jacket"
            ]
        },
        "Barfolomew": {
            "id": 55924,
            "role": "mawg",
            "items": [
                "peanut butter jar",
                "waggy tail"
            ]
        },
        "Dark Helmet": {
            "id": 99999,
            "role": "Good is dumb",
            "items": [
                "Shwartz",
                "helmet"
            ]
        },
        "Skroob": {
            "id": 12345,
            "role": "Spaceballs CEO",
            "items": [
                "luggage"
            ]
        }
    }
}

para obter os itens da Lonestar, basta fornecer um caminho separado por pontos, ou seja,

import json
from dictor import dictor

with open('test.json') as data: 
    data = json.load(data)

print dictor(data, 'characters.Lonestar.items')

>> [u'space winnebago', u'leather jacket']

você pode fornecer um valor de fallback caso a chave não esteja no caminho

há muito mais opções que você pode fazer, como ignorar letras maiúsculas e minúsculas e usar outros caracteres além de '.' como um separador de caminho,

https://github.com/perfecto25/dictor

perfecto25
fonte
0

Eu pouco mudei esta resposta. Eu adicionei checando se estamos usando lista com números. Então agora podemos usá-lo de qualquer maneira. deep_get(allTemp, [0], {})ou deep_get(getMinimalTemp, [0, minimalTemperatureKey], 26)etc

def deep_get(_dict, keys, default=None):
    def _reducer(d, key):
        if isinstance(d, dict):
            return d.get(key, default)
        if isinstance(d, list):
            return d[key] if len(d) > 0 else default
        return default
    return reduce(_reducer, keys, _dict)
Darex1991
fonte
0

Já existem muitas respostas boas, mas eu criei uma função chamada get similar to lodash get in JavaScript land, que também suporta acessar listas por índice:

def get(value, keys, default_value = None):
'''
    Useful for reaching into nested JSON like data
    Inspired by JavaScript lodash get and Clojure get-in etc.
'''
  if value is None or keys is None:
      return None
  path = keys.split('.') if isinstance(keys, str) else keys
  result = value
  def valid_index(key):
      return re.match('^([1-9][0-9]*|[0-9])$', key) and int(key) >= 0
  def is_dict_like(v):
      return hasattr(v, '__getitem__') and hasattr(v, '__contains__')
  for key in path:
      if isinstance(result, list) and valid_index(key) and int(key) < len(result):
          result = result[int(key)] if int(key) < len(result) else None
      elif is_dict_like(result) and key in result:
          result = result[key]
      else:
          result = default_value
          break
  return result

def test_get():
  assert get(None, ['foo']) == None
  assert get({'foo': 1}, None) == None
  assert get(None, None) == None
  assert get({'foo': 1}, []) == {'foo': 1}
  assert get({'foo': 1}, ['foo']) == 1
  assert get({'foo': 1}, ['bar']) == None
  assert get({'foo': 1}, ['bar'], 'the default') == 'the default'
  assert get({'foo': {'bar': 'hello'}}, ['foo', 'bar']) == 'hello'
  assert get({'foo': {'bar': 'hello'}}, 'foo.bar') == 'hello'
  assert get({'foo': [{'bar': 'hello'}]}, 'foo.0.bar') == 'hello'
  assert get({'foo': [{'bar': 'hello'}]}, 'foo.1') == None
  assert get({'foo': [{'bar': 'hello'}]}, 'foo.1.bar') == None
  assert get(['foo', 'bar'], '1') == 'bar'
  assert get(['foo', 'bar'], '2') == None
Peter Marklund
fonte