Безпечний метод Python, щоб отримати значення вкладеного словника


145

У мене вкладений словник. Чи є лише один спосіб безпечно вивести значення?

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

А може, python має такий метод, як get()для вкладеного словника?



1
Код у вашому запитанні, на мій погляд, вже найкращий спосіб вивести вкладені значення зі словника. Ви завжди можете вказати значення за замовчуванням у except keyerror:пункті.
Пітер Шорн

Відповіді:


281

Ви можете використовувати getдвічі:

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

Це повернеться, Noneякщо key1або key2не існує.

Зауважте, що це все-таки може підняти, AttributeErrorякщо example_dict['key1']існує, але не є діктом (або об'єктом, подібним до getдікта). try..exceptКод відповідав би підняти TypeErrorзамість цього , якщо example_dict['key1']це unsubscriptable.

Ще одна відмінність полягає в тому, що try...exceptкоротке замикання одразу після першого відсутнього ключа. Ланцюжок getдзвінків не робить.


Якщо ви хочете зберегти синтаксис, example_dict['key1']['key2']але не хочете, щоб він коли-небудь піднімав KeyErrors, тоді ви можете скористатися рецептом 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'>

Зауважте, що це повертає порожній хешер, коли ключ відсутній.

Оскільки Hasherце підклас, dictви можете використовувати хешер так само, як і ви dict. Доступні всі ті самі методи та синтаксис, що хеши просто по-різному ставляться до відсутніх ключів.

Ви можете перетворити звичайний dictу Hasherтакий:

hasher = Hasher(example_dict)

і конвертувати в Hasherзвичайний dictтак само легко:

regular_dict = dict(hasher)

Ще одна альтернатива - приховати неподобство в функції помічника:

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

Таким чином, решта вашого коду може бути відносно читаною:

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

38
значить, python не має прекрасного рішення для цієї справи ?:(
Arti

Я зіткнувся з проблемою з подібною реалізацією. Якщо у вас є d = {key1: None}, перше отримання повернеться None, і тоді ви отримаєте виняток): Я намагаюся знайти це рішення для цього
Хуерсіо,

1
safegetМетод у багатьох відносинах не дуже безпечних , так як він перезаписує вихідний словник, тобто ви не можете спокійно робити такі речі safeget(dct, 'a', 'b') or safeget(dct, 'a').
neverfox

4
@KurtBourbaki: dct = dct[key] переназначає нове значення для локальної змінної dct . Це не мутує оригінальний дікт (тому на оригінальний дік не впливає safeget). Якщо, з іншого боку, dct[key] = ...він був використаний, то початковий дикт був би змінений. Іншими словами, імена Python пов'язані зі значеннями . Присвоєння нового значення імені не впливає на старе значення (якщо тільки немає більше посилань на старе значення, в цьому випадку (у CPython) воно буде збирати сміття.)
unutbu

1
safegetМетод також не буде в разі , якщо ключ вкладеної Словнику існує, але значення дорівнює нулю. Це кине TypeError: 'NoneType' object is not subscriptableнаступну ітерацію
Стенлі Ф.

60

Ви також можете використати пітон зменшити :

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

5
Просто хотілося відзначити, що functools більше не є вбудованим в Python3 і його потрібно імпортувати з functools, що робить цей підхід трохи менш елегантним.
yoniLavi

3
Невелика корекція цього коментаря: зниження більше не є вбудованим в Py3. Але я не бачу, чому це робить це менш елегантним. Це робить його менш придатним для однолінійного вкладиша, але бути однолінійним не може автоматично кваліфікувати чи дискваліфікувати щось як "елегантне".
PaulMcG

30

Поєднуючи всі ці відповіді тут і невеликі зміни, які я вніс, я думаю, що ця функція була б корисною. його безпечний, швидкий, легко піддається технічному обслуговуванню.

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)

Приклад:

>>> 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
Ідеально підходить для шаблонів Jinja2
Thomas

Це хороше рішення, хоча є і недолік: навіть якщо перший ключ недоступний або значення, передане як аргумент словника функції, не є словником, функція перейде від першого елемента до останнього. В основному, це робиться у всіх випадках.
Арсеній

1
deep_get({'a': 1}, "a.b")дає, Noneале я б очікував винятку на кшталт KeyErrorабо чогось іншого.
стік потоку

@edityouprofile. тоді вам просто потрібно зробити невеликі зміни, щоб змінити значення повернення з NoneнаRaise KeyError
Yuda Prawira

15

На основі відповіді Йоава, ще безпечнішого підходу:

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

12

Рекурсивне рішення. Це не найефективніше, але я вважаю його трохи читабельнішим, ніж інші приклади, і він не покладається на функціональні функції.

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

Приклад

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

Більш відшліфована версія

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

Хоча підхід до зменшення акуратний і короткий, я вважаю, що простий цикл простіше зробити. Я також включив параметр за замовчуванням.

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

Як вправу зрозуміти, як працює одноничний редуктор, я зробив наступне. Але врешті-решт циклічний підхід мені здається більш інтуїтивним.

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)

Використання

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

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

5

Я пропоную вам спробувати python-benedict.

Це dictпідклас, який забезпечує підтримку клавіатури та багато іншого.

Установка: pip install python-benedict

from benedict import benedict

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

тепер ви можете отримати доступ до вкладених значень за допомогою keypath :

val = example_dict['key1.key2']

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

або отримати доступ до вкладених значень за допомогою списку ключів :

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

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

Це добре перевірено і з відкритим кодом на GitHub :

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


@ perfecto25 дякую! Я незабаром вийду нові функції, будьте в курсі 😉
Fabio Caccamo

@ perfecto25 Я додав підтримку до списку індексів, наприклад. d.get('a.b[0].c[-1]')
Фабіо Каккамо

4

Простий клас, який може обернути дік і отримати на основі ключа:

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

Наприклад:

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

Якщо ключа не існує, він повертається Noneза замовчуванням. Ви можете змінити це за допомогою default=ключа в FindDictобгортці - наприклад`:

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

3

для отримання ключа другого рівня, ви можете зробити це:

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

2

Побачивши це для глибокого отримання атрибутів, я зробив наступне, щоб безпечно отримати вкладені dictзначення, використовуючи крапкові позначення. Це працює для мене, оскільки мої dictsдесеріалізовані об’єкти MongoDB, тому я знаю, що імена ключів не містять .s. Крім того, в моєму контексті я можу вказати помилкове резервне значення ( None), якого я не маю в своїх даних, тому я можу уникати спроби / викрійки під час виклику функції.

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
fallbackнасправді не використовується у функції.
153957,

Зауважте, що це не працює для ключів, що містять.
JW.

Коли ми зателефонуємо obj [name], чому б не obj.get (ім'я, резервний запас) і не уникнути пробного лову (якщо ви хочете пробувати
уловку

Дякуємо @ 153957. Я полагодив це. І так @JW, це працює для мого випадку використання. Ви можете додати sep=','аргумент ключового слова для узагальнення за заданими умовами (sep, backback). І @denvar, якщо objговорять про тип intпісля послідовності зменшення, тоді obj [ім'я] викликає TypeError, який я вловлю. Якщо я використовував obj.get (ім'я) або obj.get (ім'я, запасний) замість цього, це призвело б до виникнення AttributeError, тому будь-який спосіб мені потрібно впіймати.
Донні Вінстон

1

Ще одна функція для тієї ж речі також повертає булеве значення, яке відображає, знайдено ключ чи ні, і обробляє деякі несподівані помилки.

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

Приклад використання:

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

(Правда, 'holla')

(Помилковий, '')



0

Адаптація відповіді unutbu, яку я вважаю корисною у власному коді:

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

Він створює словниковий запис для key1, якщо він вже не має цього ключа, щоб уникнути KeyError. Якщо ви хочете вписати вкладений словник, який так чи інакше включає цей ключ, як я, це здається найпростішим рішенням.


0

Оскільки підвищення ключової помилки, якщо в одному з ключів відсутня, є розумною справою, ми навіть не можемо перевірити її та отримаємо її як єдину:

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

0

Невелике вдосконалення до reduceпідходу, щоб він працював зі списком. Також використовується шлях даних у вигляді рядка, розділеного крапками замість масиву.

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

Я використовував рішення, яке схоже на подвійний get, але з додатковою здатністю уникати TypeError, використовуючи, якщо інша логіка:

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

Однак чим більше вкладений словник, тим більш громіздким він стає.


0

Для вкладених словників / пошуку JSON ви можете використовувати діктор

pip встановити діктор

об'єкт 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"
            ]
        }
    }
}

щоб отримати елементи Lonestar, просто введіть розділений крапкою шлях, тобто

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

Ви можете надати резервне значення у випадку, якщо ключ не на шляху

є багато інших варіантів, як-от ігнорувати обробку літер та використовувати інші символи, крім '.' як роздільник шляху,

https://github.com/perfecto25/dictor


0

Я трохи змінив цю відповідь. Я додав перевірку, чи використовуємо ми список із цифрами. Тож тепер ми можемо використовувати його будь-яким способом. deep_get(allTemp, [0], {})або deep_get(getMinimalTemp, [0, minimalTemperatureKey], 26)тощо

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

Вже є багато хороших відповідей, але я придумав функцію під назвою отримати схожий на lodash get in JavaScript land, який також підтримує потрапляння до списків за індексом:

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.