Metodo sicuro Python per ottenere il valore del dizionario nidificato


145

Ho un dizionario nidificato. C'è solo un modo per ottenere valori in modo sicuro?

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

O forse Python ha un metodo simile get()al dizionario nidificato?



1
Il codice nella tua domanda è, a mio avviso, già il modo migliore per ottenere valori nidificati dal dizionario. È sempre possibile specificare un valore predefinito nella except keyerror:clausola.
Peter Schorn,

Risposte:


281

Puoi usare getdue volte:

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

Questo restituirà Nonese uno key1o key2non esiste.

Si noti che ciò potrebbe comunque generare un AttributeErrorif se example_dict['key1']non è un dict (o un oggetto simile a un dict con un getmetodo). Il try..exceptcodice che hai pubblicato aumenterebbe TypeErrorinvece se non example_dict['key1']è iscrivibile.

Un'altra differenza è che i try...exceptcortocircuiti immediatamente dopo il primo tasto mancante. La catena di getchiamate no.


Se desideri preservare la sintassi, example_dict['key1']['key2']ma non vuoi che aumenti mai KeyErrors, puoi usare la ricetta di 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'>

Si noti che questo restituisce un Hasher vuoto quando manca una chiave.

Poiché Hasherè una sottoclasse di dictte puoi usare un Hasher nello stesso modo in cui potresti usare undict . Sono disponibili tutti gli stessi metodi e sintassi, gli hashter trattano le chiavi mancanti in modo diverso.

Puoi convertire un normale dictin un Hashersimile:

hasher = Hasher(example_dict)

e converti a Hasherin normale dictaltrettanto facilmente:

regular_dict = dict(hasher)

Un'altra alternativa è nascondere la bruttezza in una funzione di aiuto:

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

Quindi il resto del codice può rimanere relativamente leggibile:

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

38
quindi, Python non ha una soluzione meravigliosa per questo caso? :(
Arti

Ho riscontrato un problema con un'implementazione simile. Se hai d = {key1: None}, il primo get restituirà None e poi avrai un'eccezione): sto cercando di trovare una soluzione per questo
Huercio

1
Il safegetmetodo non è molto sicuro in molti modi poiché sovrascrive il dizionario originale, il che significa che non puoi fare cose del genere safeget(dct, 'a', 'b') or safeget(dct, 'a').
Neverfox,

4
@KurtBourbaki: dct = dct[key] riassegna un nuovo valore alla variabile locale dct . Questo non muta il dict originale (quindi il dict originale non è interessato da safeget.) Se, d'altra parte, dct[key] = ...fosse stato usato, allora il dict originale sarebbe stato modificato. In altre parole, in Python i nomi sono associati a valori . L'assegnazione di un nuovo valore a un nome non influisce sul vecchio valore (a meno che non ci siano più riferimenti al vecchio valore, nel qual caso (in CPython) verrà raccolta la spazzatura.)
unutbu

1
Il safegetmetodo fallirà anche nel caso in cui esista la chiave di un dict nidificato, ma il valore è null. Lancerà TypeError: 'NoneType' object is not subscriptablenella prossima iterazione
Stanley F.

60

Puoi anche usare Python Red :

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

5
Volevo solo ricordare che functools non è più incorporato in Python3 e deve essere importato da functools, il che rende questo approccio leggermente meno elegante.
yoniLavi,

3
Leggera correzione a questo commento: ridurre non è più incorporato in Py3. Ma non vedo perché questo renda questo meno elegante. E non lo rende meno adatto per una battuta, ma essendo una battuta non si qualifica automaticamente o squalificare qualcosa come "elegante".
PaulMcG

30

Combinando qui tutte queste risposte e le piccole modifiche che ho apportato, penso che questa funzione sarebbe utile. è sicuro, rapido, facilmente gestibile.

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)

Esempio :

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

1
Perfetto per i modelli Jinja2
Thomas

Questa è una buona soluzione sebbene ci sia anche uno svantaggio: anche se la prima chiave non è disponibile o il valore passato come argomento del dizionario alla funzione non è un dizionario, la funzione passerà dal primo elemento all'ultimo. Fondamentalmente, lo fa in tutti i casi.
Arseny,

1
deep_get({'a': 1}, "a.b")Nonema mi aspetterei un'eccezione simile KeyErroro qualcos'altro.
Stackunderflow

@edityouprofile. allora devi solo fare una piccola modifica per cambiare il valore di ritorno da NoneaRaise KeyError
Yuda Prawira il

15

Basandosi sulla risposta di Yoav, un approccio ancora più sicuro:

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

12

Una soluzione ricorsiva. Non è il più efficiente, ma lo trovo un po 'più leggibile rispetto agli altri esempi e non si basa su funzioni.

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

Esempio

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

Una versione più raffinata

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)

8

Mentre l'approccio di riduzione è pulito e breve, penso che un loop semplice sia più facile da eseguire. Ho anche incluso un parametro predefinito.

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

Come esercizio per capire come funzionava il riduttore a una fodera, ho fatto quanto segue. Ma alla fine l'approccio ad anello mi sembra più intuitivo.

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)

uso

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

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

5

Ti consiglio di provare python-benedict.

È una dictsottoclasse che fornisce supporto keypath e molto altro.

Installazione: pip install python-benedict

from benedict import benedict

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

ora puoi accedere ai valori nidificati usando keypath :

val = example_dict['key1.key2']

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

o accedi ai valori nidificati utilizzando l' elenco delle chiavi :

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

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

È ben testato e open-source su GitHub :

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


@ perfecto25 grazie! Presto pubblicherò nuove funzionalità, rimanete sintonizzati 😉
Fabio Caccamo, il

@ perfecto25 Ho aggiunto il supporto per elencare gli indici, ad es. d.get('a.b[0].c[-1]')
Fabio Caccamo,

La funzione from_toml non sembra essere implementata. E può essere difficile importare BeneDict.
DLyons

@Dyy ti sbagli, in ogni caso sentiti libero di aprire un problema su GitHub.
Fabio Caccamo

1
Sì, è tutto a posto. Peccato che mi mancasse - mi avrebbe fatto risparmiare un po 'di tempo. Benedetto sembra avere alcune funzionalità molto utili.
DLyons

4

Una classe semplice che può avvolgere un dict e recuperare in base a una chiave:

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

Per esempio:

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

Se la chiave non esiste, viene restituita Noneper impostazione predefinita. Puoi sovrascriverlo usando una default=chiave nel FindDictwrapper - ad esempio`:

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

3

per un recupero di chiave di secondo livello, puoi fare questo:

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

2

Dopo aver visto questo per ottenere gli attributi in profondità, ho fatto quanto segue per ottenere in sicurezza dictvalori nidificati usando la notazione a punti. Questo funziona per me perché i miei dictssono oggetti MongoDB deserializzati, quindi so che i nomi delle chiavi non contengono .s. Inoltre, nel mio contesto, posso specificare un valore di fallback errato ( None) che non ho nei miei dati, quindi posso evitare il modello try / tranne quando chiamo la funzione.

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)

7
fallbacknon è effettivamente utilizzato nella funzione.
153957,

Si noti che questo non funziona per le chiavi che contengono un.
JW.

Quando chiamiamo obj [nome] perché non obj.get (nome, fallback) ed evitiamo il try-catch (se vuoi il try-catch, quindi restituisci il fallback, non None)
denvar

Grazie @ 153957. L'ho riparato. E sì @JW, questo funziona per il mio caso d'uso. È possibile aggiungere una sep=','parola chiave arg per generalizzare per determinate condizioni (sep, fallback). E @denvar, se objsi dice di tipo intdopo una sequenza di riduzione, allora obj [name] genera un TypeError, che catturo. Se invece avessi usato obj.get (nome) o obj.get (nome, fallback), avrebbe generato un AttributeError, quindi in entrambi i casi avrei dovuto catturarlo.
Donny Winston,

1

Ancora un'altra funzione per la stessa cosa, restituisce anche un valore booleano per indicare se la chiave è stata trovata o meno e gestisce alcuni errori imprevisti.

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

esempio di utilizzo:

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

(Vero, 'holla')

(Falso, '')



0

Un adattamento della risposta di Unutbu che ho trovato utile nel mio codice:

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

Genera una voce di dizionario per key1 se non ha già quella chiave in modo da evitare KeyError. Se vuoi finire un dizionario nidificato che include comunque l'associazione chiave come ho fatto io, questa sembra la soluzione più semplice.


0

Dal momento che generare un errore chiave se manca una delle chiavi è una cosa ragionevole da fare, non possiamo nemmeno controllarlo e ottenerlo singolarmente:

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

0

Poco miglioramento reducenell'approccio per farlo funzionare con la lista. Utilizza anche il percorso dati come stringa divisa per punti anziché come matrice.

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)

0

Una soluzione che ho usato che è simile al doppio get ma con la possibilità aggiuntiva di evitare un TypeError usando la logica if else:

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

Tuttavia, più nidifica il dizionario, più diventa ingombrante.


0

Per il dizionario nidificato / ricerche JSON, è possibile utilizzare dictor

pip installa dictor

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

per ottenere gli oggetti di Lonestar, è sufficiente fornire un percorso separato da punti, ad es

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

puoi fornire un valore di fallback nel caso in cui la chiave non sia nel percorso

ci sono molte più opzioni che puoi fare, come ignorare il maiuscolo e usare altri caratteri oltre a "." come separatore di percorsi,

https://github.com/perfecto25/dictor


0

Ho cambiato poco questa risposta. Ho aggiunto verificando se stiamo usando la lista con i numeri. Quindi ora possiamo usarlo in qualunque modo. deep_get(allTemp, [0], {})o deep_get(getMinimalTemp, [0, minimalTemperatureKey], 26)ecc

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)

0

Ci sono già molte buone risposte, ma ho trovato una funzione chiamata get like to lodash get in JavaScript land che supporta anche il raggiungimento degli elenchi per indice:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.