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

145

J'ai un dictionnaire imbriqué. Existe-t-il un seul moyen de diffuser 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é?

Arti
la source
1
Le code de votre question est, à mon avis, déjà le meilleur moyen d'extraire des valeurs imbriquées du dictionnaire. Vous pouvez toujours spécifier une valeur par défaut dans la except keyerror:clause.
Peter Schorn le

Réponses:

281

Vous pouvez utiliser getdeux fois:

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

Cela reviendra Nonesi l'un key1ou l' autre key2n'existe pas.

Notez que cela peut toujours déclencher un AttributeErrorif example_dict['key1']existe mais n'est pas un dict (ou un objet de type dict avec une getméthode). Le try..exceptcode que vous avez publié lèverait un à la TypeErrorplace s'il example_dict['key1']est désinscriptible.

Une autre différence est que les try...exceptcourt-circuits immédiatement après la première clé manquante. La chaîne d' getappels ne le fait pas.


Si vous souhaitez conserver la syntaxe, example_dict['key1']['key2']mais ne voulez pas qu'elle déclenche jamais KeyErrors, vous pouvez utiliser la 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 renvoie un Hasher vide lorsqu'une clé est manquante.

Depuis Hasherune sous-classe de dictvous pouvez utiliser un Hasher de la même manière que vous pourriez utiliser un dict. Toutes les mêmes méthodes et syntaxe sont disponibles, les Hashers traitent simplement les clés manquantes différemment.

Vous pouvez convertir un régulier dicten un Hashercomme ceci:

hasher = Hasher(example_dict)

et convertissez a Hasheren régulier dicttout 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')
unutbu
la source
38
alors, python n'a pas de belle solution pour ce cas? :(
Arti
J'ai rencontré un problème avec une implémentation similaire. Si vous avez d = {key1: None}, le premier get retournera None et vous aurez une exception): J'essaie de trouver une solution pour cela
Huercio
1
La safegetméthode n'est pas très sûre à bien des égards car elle écrase le dictionnaire d'origine, ce qui signifie que vous ne pouvez pas faire des choses comme safeget(dct, 'a', 'b') or safeget(dct, 'a').
neverfox
4
@KurtBourbaki: dct = dct[key] réaffecte une nouvelle valeur à la variable locale dct . Cela ne modifie pas le dict original (donc le dict original n'est pas affecté par safeget.) Si, par contre, dct[key] = ...avait été utilisé, alors le dict original aurait été modifié. En d'autres termes, en Python, les noms sont liés à des valeurs . L'attribution d'une nouvelle valeur à un nom n'affecte pas l'ancienne valeur (à moins qu'il n'y ait plus de références à l'ancienne valeur, auquel cas (en CPython) il sera récupéré.)
unutbu
1
La safegetméthode échouera également dans le cas où la clé d'un dict imbriqué existe, mais la valeur est nulle. Il jettera TypeError: 'NoneType' object is not subscriptabledans la prochaine itération
Stanley F.
60

Vous pouvez également utiliser python réduire :

def deep_get(dictionary, *keys):
    return reduce(lambda d, key: d.get(key) if d else None, keys, dictionary)
Yoav T
la source
5
Je voulais juste mentionner que functools n'est plus un intégré dans Python3 et doit être importé depuis functools, ce qui rend cette approche légèrement moins élégante.
yoniLavi
3
Légère correction à ce commentaire: réduire n'est plus un intégré dans Py3. Mais je ne vois pas pourquoi cela rend cela moins élégant. Cela le rend moins approprié pour un one-liner, mais être un one-liner ne qualifie pas ou ne disqualifie pas automatiquement quelque chose comme étant "élégant".
PaulMcG
30

En combinant toutes ces réponses ici 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
>>>
Yuda Prawira
la source
1
Parfait pour les modèles Jinja2
Thomas
C'est une bonne solution mais il y a aussi un inconvénient: même si la première clé n'est pas disponible, ou si la valeur passée comme argument de dictionnaire à la fonction n'est pas un dictionnaire, la fonction passera du premier élément au dernier. En gros, il le fait dans tous les cas.
Arseny
1
deep_get({'a': 1}, "a.b")donne Nonemais je m'attendrais à une exception comme KeyErrorou autre chose.
stackunderflow
@edityouprofile. alors il vous suffit de faire une petite modification pour changer la valeur de retour de NoneàRaise KeyError
Yuda Prawira
15

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)
Jose Alban
la source
12

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 des 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 raffinée

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
la source
8

Bien que l'approche de réduction soit nette et courte, je pense qu'une simple boucle est plus facile à grok. 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

Pour comprendre comment fonctionnait le réducteur à une ligne, j'ai fait ce qui suit. Mais finalement, l'approche en 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')
zzz
la source
5

Je vous suggère d'essayer python-benedict.

C'est une dictsous-classe qui fournit un support de keypath et bien plus encore.

Installation: pip install python-benedict

from benedict import benedict

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

vous pouvez maintenant accéder aux valeurs imbriquées à l'aide du chemin d'accès :

val = example_dict['key1.key2']

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

ou accédez aux valeurs imbriquées à l'aide de la liste des clés :

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

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

Il est bien testé et open-source sur GitHub :

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

Fabio Caccamo
la source
@ perfecto25 merci! Je publierai bientôt de nouvelles fonctionnalités, restez à l'écoute 😉
Fabio Caccamo
@ perfecto25 J'ai ajouté le support pour lister les index, par exemple. d.get('a.b[0].c[-1]')
Fabio Caccamo le
La fonction from_toml ne semble pas être implémentée. Et il peut être difficile d'importer BeneDict.
DLyons il y a
@DLyons vous vous trompez, en tout cas n'hésitez pas à ouvrir un problème sur GitHub.
Fabio Caccamo il y a
1
Oui, c'est bien là. Dommage que je l'ai manqué - m'aurait fait gagner du temps. Benoît semble avoir des fonctionnalités très utiles.
DLyons
4

Une classe simple qui peut encapsuler 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 Nonepar défaut. Vous pouvez remplacer cela en utilisant undefault= clé dans le FindDictwrapper - par exemple`:

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

pour une récupération de clé de deuxième niveau, vous pouvez le faire:

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

Après avoir vu cela pour obtenir des attributs en profondeur, j'ai fait ce qui suit pour obtenir en toute sécurité des dictvaleurs imbriquées en utilisant la notation par points. Cela fonctionne pour moi parce que mes dictsobjets MongoDB sont désérialisés, donc je sais que les noms de clés ne contiennent pas de .s. De plus, dans mon contexte, je peux spécifier une valeur de repli falsifiée ( None) que je n'ai pas dans mes données, afin que je puisse éviter le modèle 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)
Donny Winston
la source
7
fallbackn'est pas réellement utilisé dans la fonction.
153957 du
Notez que cela ne fonctionne pas pour les clés qui contiennent un.
JW.
Quand nous appelons obj [nom] pourquoi pas obj.get (nom, fallback) et éviter le try-catch (si vous voulez le try-catch, alors retournez fallback, pas None)
denvar
Merci @ 153957. Je l'ai corrigé. Et oui @JW, cela fonctionne pour mon cas d'utilisation. Vous pouvez ajouter un sep=','mot - clé arg pour généraliser des conditions données (sep, fallback). Et @denvar, si objc'est dire de type intaprès une séquence de réduction, alors obj [nom] lève une TypeError, que j'attrape. Si j'utilisais à la place obj.get (nom) ou obj.get (nom, repli), cela déclencherait une AttributeError, donc de toute façon, je devrais attraper.
Donny Winston
1

Encore une autre fonction pour la même chose, renvoie également un booléen pour représenter si la clé a été trouvée ou non et gère certaines erreurs inattendues.

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

exemple d'utilisation:

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

(Vrai, 'holla')

(Faux, '')

penduDev
la source
0

Une adaptation de la réponse d'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 n'a pas déjà cette clé afin que vous évitiez l'erreur KeyError. Si vous voulez quand même créer un dictionnaire imbriqué qui inclut ce couplage de clés comme je l'ai fait, cela semble être la solution la plus simple.

GènesRus
la source
0

Puisque lever une erreur de clé si l'une des clés est manquante est une chose raisonnable à faire, nous ne pouvons même pas la vérifier et 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
Ben Usman
la source
0

Peu d'amélioration de l' reduceapproche pour le faire fonctionner avec la liste. Utiliser également le chemin de données sous forme de chaîne divisée par des points au lieu d'un tableau.

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
la source
0

Une solution que j'ai utilisée qui est similaire au double get mais avec la possibilité supplémentaire d'éviter une TypeError en utilisant la logique if else:

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

Cependant, plus le dictionnaire est imbriqué, plus cela devient compliqué.

Adam The Housman
la source
0

Pour les recherches de dictionnaire imbriquées / JSON, vous pouvez utiliser dictor

pip install dictor

objet 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"
            ]
        }
    }
}

pour obtenir les éléments de Lonestar, fournissez simplement un chemin séparé par des points, c.-à-d.

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

vous pouvez fournir une valeur de secours au cas où la clé ne serait pas dans le chemin

il y a des tonnes d'options supplémentaires que vous pouvez faire, comme ignorer la casse des lettres et utiliser d'autres caractères en plus de '.' comme séparateur de chemin,

https://github.com/perfecto25/dictor

perfecto25
la source
0

J'ai peu changé cette réponse. J'ai ajouté la vérification si nous utilisons une liste avec des nombres. Alors maintenant, nous pouvons l'utiliser de n'importe quelle manière. deep_get(allTemp, [0], {})ou 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
la source
0

Il y a déjà beaucoup de bonnes réponses, mais j'ai mis au point une fonction appelée get similar to lodash get in JavaScript land qui prend également en charge l'accès aux listes par index:

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
la source