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é?
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')
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)
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
>>>
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)
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')
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)
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')
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 ''
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)
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.
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