Bezpieczna metoda w Pythonie, aby uzyskać wartość zagnieżdżonego słownika


145

Mam zagnieżdżony słownik. Czy jest tylko jeden sposób, aby bezpiecznie uzyskać wartości?

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

A może Python ma metodę podobną get()do zagnieżdżonego słownika?



1
Moim zdaniem kod zawarty w twoim pytaniu jest już najlepszym sposobem na usunięcie zagnieżdżonych wartości ze słownika. W except keyerror:klauzuli zawsze można określić wartość domyślną .
Peter Schorn

Odpowiedzi:


281

Możesz użyć getdwa razy:

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

Zwróci to, Nonejeśli albo nie istnieje, key1albo key2nie.

Zauważ, że może to nadal wywołać AttributeErrorif example_dict['key1']istnieje, ale nie jest dict (lub obiektem podobnym do dyktu z getmetodą). try..exceptKod, który pisał by podnieść TypeErrorzamiast jeśli example_dict['key1']jest unsubscriptable.

Inną różnicą jest to, że try...exceptzwarcia natychmiast po pierwszym brakującym kluczu. Łańcuch getpołączeń nie.


Jeśli chcesz zachować składnię, example_dict['key1']['key2']ale nie chcesz, aby kiedykolwiek generowała błędy KeyErrors, możesz użyć przepisu 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'>

Zwróć uwagę, że zwraca to pusty hasher, gdy brakuje klucza.

Ponieważ Hasherjest to podklasa dict, możesz używać Hashera w taki sam sposób, w jaki możesz użyć dict. Wszystkie te same metody i składnia są dostępne, Hashery po prostu traktują brakujące klucze inaczej.

Możesz przekształcić zwykły dictw Hashertaki:

hasher = Hasher(example_dict)

i równie łatwo przekonwertuj a Hasherna zwykły dict:

regular_dict = dict(hasher)

Inną alternatywą jest ukrycie brzydoty w funkcji pomocniczej:

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

Więc reszta twojego kodu może pozostać stosunkowo czytelna:

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

38
więc python nie ma pięknego rozwiązania w tym przypadku? :(
Arti

Napotkałem problem z podobną implementacją. Jeśli masz d = {key1: None}, pierwsze get zwróci None, a potem będziesz miał wyjątek): Próbuję znaleźć rozwiązanie tego
Huercio

1
Ta safegetmetoda jest pod wieloma względami niezbyt bezpieczna, ponieważ zastępuje oryginalny słownik, co oznacza, że ​​nie można bezpiecznie wykonywać takich rzeczy jak safeget(dct, 'a', 'b') or safeget(dct, 'a').
neverfox,

4
@KurtBourbaki: dct = dct[key] ponownie przypisuje nową wartość do zmiennej lokalnej dct . Nie powoduje to mutacji oryginalnego dyktowania (więc oryginalny dykt pozostaje nienaruszony safeget). Gdyby, z drugiej strony, dct[key] = ...został użyty, oryginalny dykt zostałby zmodyfikowany. Innymi słowy, w Pythonie nazwy są powiązane z wartościami . Przypisanie nowej wartości do nazwy nie wpływa na starą wartość (chyba że nie ma już odniesień do starej wartości, w którym to przypadku (w CPythonie) zostanie zebrana śmieci.)
unutbu

1
safegetMetoda nie powiedzie się także w przypadku, gdy klucz zagnieżdżonych dict istnieje, ale wartość jest null. Rzuci TypeError: 'NoneType' object is not subscriptablekolejną iterację
Stanley F.

60

Możesz także użyć Pythona Redukcja :

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

5
Chciałem tylko wspomnieć, że functools nie jest już wbudowany w Python3 i musi zostać zaimportowany z functools, co sprawia, że ​​to podejście jest nieco mniej eleganckie.
yoniLavi

3
Nieznaczna korekta tego komentarza: redukcja nie jest już wbudowana w Py3. Ale nie rozumiem, dlaczego to czyni to mniej eleganckim. To nie sprawiają, że mniej korzystne dla jednego-liner, ale będąc jedną wkładkę nie kwalifikują się automatycznie lub zdyskwalifikować coś jako „elegancki”.
PaulMcG

30

Łącząc wszystkie te odpowiedzi tutaj i małe zmiany, które wprowadziłem, myślę, że ta funkcja byłaby przydatna. jest bezpieczny, szybki i łatwy w utrzymaniu.

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)

Przykład:

>>> 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
Idealny dla szablonów Jinja2
Thomas

Jest to dobre rozwiązanie, chociaż ma też wadę: nawet jeśli pierwszy klucz nie jest dostępny lub wartość przekazana jako argument słownikowy do funkcji nie jest słownikiem, funkcja przejdzie od pierwszego elementu do ostatniego. Zasadniczo robi to we wszystkich przypadkach.
Arseny

1
deep_get({'a': 1}, "a.b")daje, Noneale spodziewałbym się wyjątku KeyErrorlub czegoś innego.
stackunderflow

@edityouprofile. następnie wystarczy dokonać niewielkiej modyfikacji, aby zmienić wartość zwracaną z NonenaRaise KeyError
Yuda Prawira

15

Opierając się na odpowiedzi Yoav, jeszcze bezpieczniejsze podejście:

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

12

Rozwiązanie rekurencyjne. To nie jest najbardziej wydajne, ale uważam, że jest nieco bardziej czytelne niż inne przykłady i nie opiera się na functools.

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

Przykład

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

Bardziej dopracowana wersja

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

Chociaż podejście redukujące jest zgrabne i krótkie, myślę, że prostą pętlę łatwiej jest opanować. Dołączyłem również domyślny parametr.

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

Aby zrozumieć, jak działa redukcja jednowarstwowa, wykonałem następujące czynności. Ale ostatecznie podejście pętlowe wydaje mi się bardziej intuicyjne.

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)

Stosowanie

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

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

5

Proponuję spróbować python-benedict.

Jest to dictpodklasa, która zapewnia obsługę ścieżki klucza i wiele więcej.

Instalacja: pip install python-benedict

from benedict import benedict

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

teraz możesz uzyskać dostęp do zagnieżdżonych wartości za pomocą keypath :

val = example_dict['key1.key2']

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

lub uzyskaj dostęp do zagnieżdżonych wartości za pomocą listy kluczy :

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

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

Jest dobrze przetestowany i open-source na GitHub :

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


@ perfecto25 dziękuję! Wkrótce wydam nowe funkcje, bądźcie czujni 😉
Fabio Caccamo

@ perfecto25 Dodałem obsługę indeksów list, np. d.get('a.b[0].c[-1]')
Fabio Caccamo

4

Prosta klasa, która może zawijać dyktando i pobierać na podstawie klucza:

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

Na przykład:

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

Jeśli klucz nie istnieje, Nonedomyślnie zwraca . Możesz to zmienić za pomocą default=klucza w FindDictopakowaniu - na przykład:

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

3

aby odzyskać klucz drugiego poziomu, możesz to zrobić:

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

2

Po zapoznaniu się z tym, aby uzyskać szczegółowe informacje o atrybutach, wykonałem następujące czynności, aby bezpiecznie uzyskać zagnieżdżone dictwartości za pomocą notacji kropkowej. To działa dla mnie, ponieważ moje dictssą zdeserializowane obiekty MongoDB, więc wiem, że nazwy kluczy nie zawierają .s. Ponadto w moim kontekście mogę określić fałszywą wartość rezerwową ( None), której nie mam w moich danych, dzięki czemu mogę uniknąć wzorca try / except podczas wywoływania funkcji.

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
fallbacknie jest faktycznie używany w funkcji.
153957

Zauważ, że nie działa to dla kluczy zawierających.
JW.

Kiedy wywołujemy obj [name], dlaczego nie obj.get (name, fallback) i unikamy try-catch (jeśli chcesz try-catch, zwróć fallback, a nie None)
denvar

Dzięki @ 153957. Naprawiłem to. I tak @JW, to działa w moim przypadku użycia. Możesz dodać sep=','słowo kluczowe arg, aby uogólnić dla podanych (sep, fallback) warunków. A @denvar, jeśli objpowiedziano, że typ intpo sekwencji redukcji, to obj [nazwa] wywołuje błąd TypeError, który łapię. Gdybym zamiast tego użył obj.get (name) lub obj.get (name, fallback), podniósłby AttributeError, więc tak czy inaczej musiałbym złapać.
Donny Winston

1

Jeszcze inna funkcja dla tej samej rzeczy również zwraca wartość logiczną, która wskazuje, czy klucz został znaleziony, czy nie, i obsługuje nieoczekiwane błędy.

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

przykład użycia:

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

(Prawda, „holla”)

(Fałszywe, '')



0

Adaptacja odpowiedzi unutbu, która okazała się przydatna w moim własnym kodzie:

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

Generuje wpis słownikowy dla key1, jeśli nie ma już tego klucza, aby uniknąć błędu KeyError. Jeśli chcesz skończyć ze słownikiem zagnieżdżonym, który i tak zawiera parowanie kluczy, tak jak ja, wydaje się to najłatwiejszym rozwiązaniem.


0

Ponieważ zgłoszenie błędu klucza, jeśli brakuje jednego z kluczy, jest rozsądną rzeczą do zrobienia, nie możemy go nawet sprawdzić i uzyskać tak pojedynczego:

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

0

Niewielka poprawa w reducepodejściu do listy. Używa również ścieżki danych jako ciągu podzielonego kropkami zamiast tablicy.

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

Rozwiązanie, którego użyłem, jest podobne do double get, ale z dodatkową możliwością uniknięcia błędu TypeError przy użyciu logiki if else:

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

Jednak im bardziej zagnieżdżony jest słownik, tym bardziej staje się to nieporęczne.


0

W przypadku zagnieżdżonych wyszukiwań w słowniku / JSON można użyć dyktatora

pip install dictor

obiekt dyktowania

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

aby otrzymać elementy Lonestar, wystarczy podać ścieżkę oddzieloną kropkami, tj

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

możesz podać wartość rezerwową w przypadku, gdy klucz nie znajduje się w ścieżce

jest mnóstwo innych opcji, które możesz zrobić, takich jak ignorowanie wielkości liter i używanie innych znaków oprócz „”. jako separator ścieżki,

https://github.com/perfecto25/dictor


0

Niewiele zmieniłem odpowiedź. Dodałem sprawdzenie, czy używamy listy z numerami. Więc teraz możemy go używać w dowolny sposób. deep_get(allTemp, [0], {})lub deep_get(getMinimalTemp, [0, minimalTemperatureKey], 26)itp

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

Jest już wiele dobrych odpowiedzi, ale wymyśliłem funkcję o nazwie get podobną do lodash get w JavaScript land, która obsługuje również sięganie do list według indeksu:

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.