web-dev-qa-db-fra.com

Un moyen élégant de vérifier si une clé imbriquée existe dans un dict en python

Existe-t-il un moyen plus lisible de vérifier si une clé enfouie dans un dict existe sans vérifier chaque niveau indépendamment?

Disons que je dois obtenir cette valeur dans un objet enterré (exemple tiré de Wikidata):

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

Pour vous assurer que cela ne se termine pas par une erreur d'exécution, il est nécessaire de vérifier chaque niveau comme suit:

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

L’autre façon de résoudre ce problème est d’englober cela dans une construction try catch qui, à mon avis, est également assez délicate pour une tâche aussi simple.

Je cherche quelque chose comme:

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

qui retourne True si tous les niveaux existent.

29
loomi

Pour être bref, avec Python, vous devez savoir qu’il est plus facile de demander pardon que l’autorisation

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

La réponse

Voici comment je traite les clés dict imbriquées:

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

Exemple:

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

Sortie:

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

Il boucle dans element donné en testant chaque clé dans un ordre donné.

Je préfère ceci à toutes les méthodes variable.get('key', {}) que j'ai trouvées car elles suivent EAFP .

Fonction sauf à être appelée comme: keys_exists(dict_element_to_test, 'key_level_0', 'key_level_1', 'key_level_n', ..). Au moins deux arguments sont requis, l'élément et une clé, mais vous pouvez ajouter le nombre de clés souhaité.

Si vous devez utiliser un type de carte, vous pouvez faire quelque chose comme:

expected_keys = ['spam', 'Egg', 'bacon']
keys_exists(data, *expected_keys)
56
Arount

Vous pouvez utiliser .get avec les valeurs par défaut:

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

mais c'est presque certainement moins clair que d'utiliser try/except.

9
Daniel Roseman

Try/except semble être le moyen le plus pythonique de le faire.
La fonction récursive suivante devrait fonctionner (ne renvoie aucune si l'une des clés n'a pas été trouvée dans le 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
4
Maurice Meyer

Vous pouvez utiliser pydash pour vérifier s’il existe: http://pydash.readthedocs.io/en/latest/api.html#pydash.objects.has

Ou obtenez la valeur (vous pouvez même définir default - à renvoyer si n'existe pas): http://pydash.readthedocs.io/en/latest/api.html#pydash.objects.has

Voici un exemple:

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

J'ai écrit une bibliothèque d'analyse de données appelée dataknead pour des cas comme celui-ci, essentiellement parce que je suis frustré par le JSON, l'API Wikidata est également renvoyée.

Avec cette bibliothèque, vous pourriez faire quelque chose comme ça

from dataknead import Knead

numid = Knead(s).query("mainsnak/datavalue/value/numeric-id").data()

if numid:
    # Do something with `numeric-id`
1
Husky

La méthode try/except est la plus propre, pas de contestation. Cependant, cela compte aussi comme une exception dans mon IDE, qui arrête l'exécution pendant le débogage. 

De plus, je n'aime pas utiliser les exceptions comme instructions de contrôle intégrées à la méthode, ce qui correspond essentiellement à ce qui se passe avec try/catch.

Voici une solution courte qui n'utilise pas de récursivité et supporte une valeur par défaut:

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
0
Houen