Безопасный метод Python для получения значения вложенного словаря

У меня есть вложенный словарь. Есть только один способ безопасно вывести ценности?

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

Или, возможно, в Python есть метод, подобный get() для вложенного словаря?

Ответ 1

Вы можете использовать get дважды:

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

Это вернет None, если не существует key1 или key2.

Обратите внимание, что это все равно может привести к появлению AttributeError, если example_dict['key1'] существует, но не является dict (или диктоподобным объектом с методом get). Вы отправили try..except код, который вы отправили, вместо TypeError, если example_dict['key1'] не подлежит подписке.

Другое отличие состоит в том, что короткое замыкание try...except сразу после первого отсутствующего ключа. Цепочка вызовов get не работает.


Если вы хотите сохранить синтаксис example_dict['key1']['key2'], но не хотите, чтобы он когда-либо поднимал KeyErrors, вы можете использовать рецепт Хашера:

class Hasher(dict):
    # /questions/35890/generating-dictionary-keys-on-the-fly/261439#261439
    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, когда отсутствует ключ.

Так как Hasher является подклассом dict, вы можете использовать Hasher так же, как вы могли бы использовать dict. Все те же методы и синтаксис доступны, Хашеры просто обрабатывают отсутствующие ключи по-разному.

Вы можете преобразовать обычный dict в Hasher следующим образом:

hasher = Hasher(example_dict)

и конвертировать a 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')

Ответ 2

Вы также можете использовать python reduce:

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

Ответ 3

Объединив все эти ответы здесь и небольшие изменения, которые я сделал, я думаю, что эта функция будет полезна. его безопасный, быстрый, легко ремонтируемый.

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

Ответ 4

Основываясь на ответе Йоава, еще более безопасный подход:

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

Ответ 5

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

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

Ответ 6

Рекурсивное решение. Он не самый эффективный, но я считаю его более читабельным, чем другие примеры, и он не зависит от functools.

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)

Ответ 7

для получения второго ключа уровня, вы можете сделать это:

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

Ответ 8

Простой класс, который может обернуть dict и получить на основе ключа:

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

Ответ 9

После просмотра этого для глубокого получения атрибутов я сделал следующее, чтобы безопасно получить вложенные значения dict, используя точечную нотацию. Это работает для меня, потому что мои dicts являются десериализованными объектами MongoDB, поэтому я знаю, что имена ключей не содержат . s. Кроме того, в моем контексте я могу указать значение ложного возврата (None), которое у меня нет в моих данных, поэтому я могу избежать шаблона try/except при вызове функции.

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)

Ответ 10

Адаптация ответа unutbu, который я нашел полезным в своем собственном коде:

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

Он генерирует словарную запись для key1, если у него еще нет этого ключа, чтобы вы избежали KeyError. Если вы хотите в конечном итоге создать словарь, включающий такое сочетание клавиш, как я, это кажется самым простым решением.

Ответ 11

Поскольку создание ключевой ошибки, если один из ключей отсутствует, является разумной вещью, мы можем даже не проверить ее и получить ее как единственную:

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