Método seguro de Python para obtener el valor del diccionario anidado

144

Tengo un diccionario anidado. ¿Hay una sola forma de obtener valores de forma segura?

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

¿O tal vez Python tiene un método como get()para diccionario anidado?

Arti
fuente
Consulte también: stackoverflow.com/questions/14692690/…
dreftymac
1
El código en su pregunta es, en mi opinión, ya la mejor manera de obtener valores anidados del diccionario. Siempre puede especificar un valor predeterminado en la except keyerror:cláusula.
Peter Schorn

Respuestas:

280

Puedes usar getdos veces:

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

Esto volverá Nonesi existe key1o key2no existe.

Tenga en cuenta que esto aún podría generar un AttributeErrorif example_dict['key1'], pero no es un dict (o un objeto similar a un dict con un getmétodo). El try..exceptcódigo que publicó generaría un TypeErrorlugar si no example_dict['key1']se puede suscribir.

Otra diferencia es que los try...exceptcortocircuitos inmediatamente después de la primera clave faltante. La cadena de getllamadas no.


Si desea preservar la sintaxis, example_dict['key1']['key2']pero no desea que aumente nunca KeyErrors, puede usar la receta 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'>

Tenga en cuenta que esto devuelve un Hasher vacío cuando falta una clave.

Dado que Hasheres una subclase de dictusted, puede usar un Hasher de la misma manera que podría usar un dict. Todos los mismos métodos y sintaxis están disponibles, Hashers solo trata las claves faltantes de manera diferente.

Puede convertir una regular dicten una Hashercomo esta:

hasher = Hasher(example_dict)

y convierta a Hashera regular con la dictmisma facilidad:

regular_dict = dict(hasher)

Otra alternativa es ocultar la fealdad en una función auxiliar:

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

Entonces, el resto de su código puede permanecer relativamente legible:

safeget(example_dict, 'key1', 'key2')
unutbu
fuente
37
Entonces, ¿Python no tiene una solución hermosa para este caso? :(
Arti
Me encontré con un problema con una implementación similar. Si tiene d = {key1: None}, el primer get devolverá None y luego tendrá una excepción): estoy tratando de encontrar una solución para esto
Huercio
1
En safegetmuchos sentidos, el método no es muy seguro, ya que sobrescribe el diccionario original, lo que significa que no puede hacer cosas con seguridad safeget(dct, 'a', 'b') or safeget(dct, 'a').
neverfox
safegetnunca sobrescribe el diccionario original. Devolverá el diccionario original, un valor del diccionario original o None.
unutbu
44
@KurtBourbaki: dct = dct[key] reasigna un nuevo valor a la variable local dct . Esto no muta el dict original (por lo que el dict original no se ve afectado por safeget). Si, por otro lado, se dct[key] = ...hubiera utilizado, entonces el dict original se habría modificado. En otras palabras, en Python los nombres están vinculados a valores . Asignación de un nuevo valor a un nombre no afecta el valor de edad (a menos que ya no hay más referencias al antiguo valor, en cuyo caso (en CPython) que conseguirá basura recogida.)
unutbu
60

También podría usar python reduce :

def deep_get(dictionary, *keys):
    return reduce(lambda d, key: d.get(key) if d else None, keys, dictionary)
Yoav T
fuente
55
Solo quería mencionar que functools ya no está integrado en Python3 y debe importarse desde functools, lo que hace que este enfoque sea un poco menos elegante.
yoniLavi
3
Ligera corrección a este comentario: reducir ya no está integrado en Py3. Pero no veo por qué esto hace que esto sea menos elegante. Lo hace menos adecuado para una línea, pero ser una línea no califica o descalifica automáticamente algo como "elegante".
PaulMcG
30

Al combinar todas estas respuestas aquí y los pequeños cambios que hice, creo que esta función sería útil. Es seguro, rápido y fácil de mantener.

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)

Ejemplo:

>>> 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
fuente
1
Perfecto para las plantillas Jinja2
Thomas
Esta es una buena solución, aunque también tiene una desventaja: incluso si la primera clave no está disponible, o si el valor pasado como argumento de diccionario a la función no es un diccionario, la función pasará del primer elemento al último. Básicamente, hace esto en todos los casos.
Arseny
1
deep_get({'a': 1}, "a.b")da Nonepero esperaría una excepción como KeyErroro algo más.
stackunderflow
@edityouprofile. entonces solo necesita hacer una pequeña modificación para cambiar el valor de retorno de NoneaRaise KeyError
Yuda Prawira
15

A partir de la respuesta de Yoav, un enfoque aún más seguro:

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

Una solución recursiva. No es el más eficiente, pero me parece un poco más legible que los otros ejemplos y no se basa en functools.

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

Ejemplo

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

Una versión más pulida.

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

Si bien el enfoque de reducción es ordenado y breve, creo que un bucle simple es más fácil de asimilar. También he incluido un parámetro predeterminado.

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 ejercicio para comprender cómo funcionaba la reducción de una línea, hice lo siguiente. Pero en última instancia, el enfoque de bucle me parece más intuitivo.

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
fuente
5

Te sugiero que pruebes python-benedict .

Es un dict subclase que proporciona soporte de keypath y mucho más.

Instalación: pip install python-benedict

from benedict import benedict

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

ahora puede acceder a valores anidados utilizando keypath :

val = example_dict['key1.key2']

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

o acceder a valores anidados utilizando la lista de claves :

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

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

Está bien probado y de código abierto en GitHub :

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

Fabio Caccamo
fuente
@ perfecto25 gracias! Pronto lanzaré nuevas funciones, estad atentos 😉
Fabio Caccamo
@ perfecto25 Agregué soporte para enumerar índices, por ejemplo. d.get('a.b[0].c[-1]')
Fabio Caccamo
4

Una clase simple que puede ajustar un dict y recuperar en función de una clave:

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 ejemplo:

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

Si la clave no existe, regresa Nonepor defecto. Puede anular eso usando una default=clave en el FindDictcontenedor, por ejemplo`:

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

para una recuperación de clave de segundo nivel, puede hacer esto:

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

Después de ver esto para obtener atributos profundos, hice lo siguiente para obtener dictvalores anidados de manera segura usando la notación de puntos. Esto funciona para mí porque dictsson objetos MongoDB deserializados, así que sé que los nombres de las claves no contienen .s. Además, en mi contexto, puedo especificar un valor de respaldo falso ( None) que no tengo en mis datos, por lo que puedo evitar el patrón try / except al llamar a la función.

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
fuente
77
fallbacken realidad no se usa en la función.
153957
Tenga en cuenta que esto no funciona para las claves que contienen un.
JW.
Cuando llamamos a obj [nombre], ¿por qué no obj.get (nombre, reserva) y evitamos el try-catch (si desea el try-catch, luego devuelva fallback, no None)
denvar
Gracias @ 153957. Lo arreglé. Y sí @JW, esto funciona para mi caso de uso. Puede agregar una sep=','palabra clave arg para generalizar para determinadas condiciones (sep, fallback). Y @denvar, si objse dice de tipo intdespués de una secuencia de reducción, entonces obj [nombre] genera un TypeError, que atrapo. Si usé obj.get (name) u obj.get (name, fallback) en su lugar, generaría un AttributeError, por lo que de cualquier manera tendría que atraparlo.
Donny Winston
1

Otra función para la misma cosa, también devuelve un valor booleano para representar si se encontró la clave o no y maneja algunos errores 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

ejemplo 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', ''))

(Cierto, 'holla')

(Falso '')

penduDev
fuente
0

Una adaptación de la respuesta de unutbu que encontré útil en mi propio código:

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

Genera una entrada de diccionario para la clave1 si aún no tiene esa clave para evitar KeyError. Si quieres terminar un diccionario anidado que incluye ese emparejamiento de teclas de todos modos como lo hice yo, esta parece ser la solución más fácil.

GenesRus
fuente
0

Dado que es razonable hacer un error de clave si falta una de las claves, incluso no podemos verificarlo y obtenerlo de la siguiente manera:

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

Poca mejora en el reduceenfoque para que funcione con la lista. También se utiliza la ruta de datos como cadena dividida por puntos en lugar 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
fuente
0

Una solución que he usado que es similar al doble get pero con la capacidad adicional de evitar un TypeError usando la lógica if else:

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

Sin embargo, cuanto más anidado está el diccionario, más engorroso se vuelve.

Adam The Housman
fuente
0

Para búsquedas anidadas de diccionario / JSON, puede usar dictor

pip install dictor

objeto dict

{
    "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 obtener los elementos de Lonestar, simplemente proporcione una ruta separada por puntos, es decir

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']

puede proporcionar un valor de reserva en caso de que la clave no esté en la ruta

Hay muchas más opciones que puedes hacer, como ignorar el uso de mayúsculas y minúsculas y usar otros caracteres además de '.' como un separador de ruta,

https://github.com/perfecto25/dictor

perfecto25
fuente
0

Cambié poco esta respuesta. Agregué verificar si estamos usando la lista con números. Así que ahora podemos usarlo de cualquier manera. deep_get(allTemp, [0], {})o 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
fuente
0

Ya hay muchas buenas respuestas, pero se me ocurrió una función llamada get similar to lodash get en JavaScript land que también permite acceder a las 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
fuente