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

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

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

или, может быть, python имеет такой метод, как get() для вложенных словарь ?

11 ответов


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

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

возвращает None если key1 или key2 не существует.

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

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


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

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 вы можете использовать Hasher почти так же, как вы могли бы использовать 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')

вы также можете использовать Python уменьшить:

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

построение ответа Йоава, еще более безопасный подход:

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

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

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

в качестве упражнения, чтобы понять, как работает reduce one-liner, я сделал следующее. Но в конечном счете петлевой подход кажется мне более интуитивным.

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

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

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

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

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

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

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

увидев этой для глубокого получения атрибутов я сделал следующее, чтобы безопасно получить вложенные 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)

рекурсивное решение. Это не самый эффективный, но я нахожу его немного более читаемым, чем другие примеры, и он не полагается на функции.

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)

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

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

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


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

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