web-dev-qa-db-fra.com

Python méthode sûre pour obtenir la valeur du dictionnaire imbriqué

J'ai un dictionnaire imbriqué. Existe-t-il un seul moyen de transmettre les valeurs en toute sécurité?

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

Ou peut-être que python a une méthode comme get() pour le dictionnaire imbriqué?

103
Arti

Vous pouvez utiliser get deux fois:

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

Ceci retournera None si key1 ou key2 n'existe pas.

Notez que cela peut toujours déclencher une AttributeError si example_dict['key1'] existe mais n'est pas un dict (ou un objet de type dict avec une méthode get). Le code try..except que vous avez posté lèverait un TypeError à la place si example_dict['key1'] est inscriptible.

Une autre différence est que le try...except court-circuite immédiatement après la première clé manquante. La chaîne d'appels get ne le fait pas.


Si vous souhaitez conserver la syntaxe, example_dict['key1']['key2'] mais que vous ne voulez pas qu'il lève jamais KeyErrors, vous pouvez utiliser le recette 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'>

Notez que cela retourne un Hasher vide lorsqu'une clé est manquante.

Puisque Hasher est une sous-classe de dict, vous pouvez utiliser un Hasher de la même manière que vous pourriez utiliser un dict. Toutes les méthodes et syntaxes sont disponibles, Hashers traite simplement les clés manquantes différemment.

Vous pouvez convertir un dict normal en un Hasher comme ceci:

hasher = Hasher(example_dict)

et convertir un Hasher en un dict tout aussi facilement:

regular_dict = dict(hasher)

Une autre alternative est de cacher la laideur dans une fonction d'assistance:

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

Ainsi, le reste de votre code peut rester relativement lisible:

safeget(example_dict, 'key1', 'key2')
196
unutbu

Vous pouvez aussi utiliser python réduire :

def deep_get(dictionary, *keys):
    return reduce(lambda d, key: d.get(key) if d else None, keys, dictionary)
47
Yoav T

En combinant toutes ces réponses et les petits changements que j'ai apportés, je pense que cette fonction serait utile. c'est sûr, rapide, facilement maintenable.

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)

Exemple :

>>> 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
>>>
17
Yuda Prawira

S'appuyant sur la réponse de Yoav, une approche encore plus sûre:

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

Bien que l'approche de réduction soit nette et courte, je pense qu'une simple boucle est plus facile à maîtriser. J'ai également inclus un paramètre par défaut.

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

Comme exercice pour comprendre le fonctionnement de la ligne de réduction, j'ai fait ce qui suit. Mais finalement, l’approche de la boucle me semble plus intuitive.

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)

Usage

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

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

Une solution récursive. Ce n'est pas le plus efficace, mais je le trouve un peu plus lisible que les autres exemples et il ne repose pas sur functools.

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

Exemple

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

Une version plus polie

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)
6
Pithikos

pour récupérer une clé de deuxième niveau, vous pouvez procéder comme suit:

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

Une classe simple qui peut envelopper un dict et récupérer en fonction d'une clé:

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

Par exemple:

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

Si la clé n'existe pas, elle retourne None par défaut. Vous pouvez remplacer cela en utilisant une clé default= dans le wrapper FindDict - par exemple`:

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

Après avoir vu this pour obtenir les attributs en profondeur, j'ai procédé comme suit pour obtenir en toute sécurité des valeurs imbriquées dict à l'aide de la notation par points. Cela fonctionne pour moi parce que mes dicts sont des objets MongoDB désérialisés. Je sais donc que les noms de clé ne contiennent pas .s. De plus, dans mon contexte, je peux spécifier une valeur de repli falsy (None) que je n'ai pas dans mes données, afin d'éviter le motif try/except lors de l'appel de la fonction.

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)
2
Donny Winston

Une adaptation de la réponse de unutbu que j'ai trouvée utile dans mon propre code:

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

Il génère une entrée de dictionnaire pour key1 s'il ne l'a pas déjà, évitant ainsi KeyError. Si vous voulez créer un dictionnaire imbriqué qui inclut cette paire de clés comme je l’ai fait, cela semble être la solution la plus simple.

0
GenesRus

Comme il est raisonnable de générer une erreur de clé si l’une des clés manque, il est même impossible de la vérifier et de l’obtenir aussi simple que cela:

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