Una forma elegante de comprobar si existe una clave anidada en un dict de python

¿Hay alguna forma más legible de verificar si existe una clave enterrada en un dict sin verificar cada nivel de forma independiente?

Digamos que necesito obtener este valor en un objeto enterrado (ejemplo tomado de Wikidata):

x = s['mainsnak']['datavalue']['value']['numeric-id'] 

Para asegurarse de que esto no termine con un error de tiempo de ejecución, es necesario verificar cada nivel así:

 if 'mainsnak' in s and 'datavalue' in s['mainsnak'] and 'value' in s['mainsnak']['datavalue'] and 'nurmeric-id' in s['mainsnak']['datavalue']['value']: x = s['mainsnak']['datavalue']['value']['numeric-id'] 

La otra forma en la que puedo pensar para resolver esto es envolver esto en una construcción de try catch que creo que también es bastante incómoda para una tarea tan simple.

Estoy buscando algo como:

 x = exists(s['mainsnak']['datavalue']['value']['numeric-id']) 

que devuelve True si todos los niveles existen.

Para ser breve, con Python debes confiar en que es más fácil pedir perdón que permiso

 try: x = s['mainsnak']['datavalue']['value']['numeric-id'] except KeyError: pass 

La respuesta

Aquí es cómo trato con las claves dict anidadas:

 def keys_exists(element, *keys): ''' Check if *keys (nested) exists in `element` (dict). ''' if type(element) is not dict: raise AttributeError('keys_exists() expects dict as first argument.') if len(keys) == 0: raise AttributeError('keys_exists() expects at least two arguments, one given.') _element = element for key in keys: try: _element = _element[key] except KeyError: return False return True 

Ejemplo:

 data = { "spam": { "egg": { "bacon": "Well..", "sausages": "Spam egg sausages and spam", "spam": "does not have much spam in it" } } } print 'spam (exists): {}'.format(keys_exists(data, "spam")) print 'spam > bacon (do not exists): {}'.format(keys_exists(data, "spam", "bacon")) print 'spam > egg (exists): {}'.format(keys_exists(data, "spam", "egg")) print 'spam > egg > bacon (exists): {}'.format(keys_exists(data, "spam", "egg", "bacon")) 

Salida:

 spam (exists): True spam > bacon (do not exists): False spam > egg (exists): True spam > egg > bacon (exists): True 

Hace un bucle en el element dado element probando cada tecla en un orden dado.

Prefiero esto a todos los métodos variable.get('key', {}) que encontré porque sigue a EAFP .

Función excepto para ser llamada como: keys_exists(dict_element_to_test, 'key_level_0', 'key_level_1', 'key_level_n', ..) . Se requieren al menos dos argumentos, el elemento y una clave, pero puede agregar la cantidad de claves que desee.

Si necesitas usar una especie de mapa, puedes hacer algo como:

 expected_keys = ['spam', 'egg', 'bacon'] keys_exists(data, *expected_keys) 

Podrías usar .get con los valores por defecto:

 s.get('mainsnak', {}).get('datavalue', {}).get('value', {}).get('numeric-id') 

pero esto es casi ciertamente menos claro que usar try / except.

Probar / excepto parece ser la forma más pythonica de hacer eso.
La siguiente función recursiva debería funcionar (devuelve Ninguna si una de las claves no se encontró en el dict):

 def exists(obj, chain): _key = chain.pop(0) if _key in obj: return exists(obj[_key], chain) if chain else obj[_key] myDict ={ 'mainsnak': { 'datavalue': { 'value': { 'numeric-id': 1 } } } } result = exists(myDict, ['mainsnak', 'datavalue', 'value', 'numeric-id']) print(result) >>> 1 

Puede usar pydash para verificar si existe: http://pydash.readthedocs.io/en/latest/api.html#pydash.objects.has

O obtenga el valor (incluso puede establecer el valor predeterminado, para devolverlo si no existe): http://pydash.readthedocs.io/en/latest/api.html#pydash.objects.has

Aquí hay un ejemplo:

 >>> get({'a': {'b': {'c': [1, 2, 3, 4]}}}, 'abc[1]') 2 

Escribí una biblioteca de análisis de datos llamada dataknead para casos como este, básicamente porque me dataknead frustrado por el JSON que también devuelve la API de Wikidata.

Con esa biblioteca podrías hacer algo como esto.

 from dataknead import Knead numid = Knead(s).query("mainsnak/datavalue/value/numeric-id").data() if numid: # Do something with `numeric-id` 

El modo try / except es el más limpio, no hay concurso. Sin embargo, también cuenta como una excepción en mi IDE, que detiene la ejecución durante la depuración.

Además, no me gusta usar excepciones como declaraciones de control en el método, que es esencialmente lo que está sucediendo con el bash / captura.

Aquí hay una solución corta que no usa la recursión y admite un valor predeterminado:

 def chained_dict_lookup(lookup_dict, keys, default=None): _current_level = lookup_dict for key in keys: if key in _current_level: _current_level = _current_level[key] else: return default return _current_level