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

156

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

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

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

Арти
источник
См. Также: stackoverflow.com/questions/14692690/…
dreftymac
1
На мой взгляд, код в вашем вопросе уже является лучшим способом получить вложенные значения из словаря. Вы всегда можете указать в предложении значение по умолчанию except keyerror:.
Питер Шорн,

Ответы:

308

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

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

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

Обратите внимание, что это все еще может вызвать AttributeErrorif, example_dict['key1']но не является dict (или dict-подобным объектом с 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вы можете использовать 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')
Unutbu
источник
41
Итак, у python нет красивого решения для этого случая? :(
Arti
У меня возникла проблема с аналогичной реализацией. Если у вас есть d = {key1: None}, первое get вернет None, и тогда у вас будет исключение): Я пытаюсь найти решение для этого
Huercio
1
Этот safegetметод во многих отношениях не очень безопасен, поскольку он перезаписывает исходный словарь, а это означает, что вы не можете безопасно делать такие вещи, как safeget(dct, 'a', 'b') or safeget(dct, 'a').
neverfox
4
@KurtBourbaki: dct = dct[key] переназначает новое значение локальной переменной dct . Это не изменяет исходный dict (поэтому исходный dict не изменяется safeget). Если бы, с другой стороны, dct[key] = ...использовался, то исходный dict был бы изменен. Другими словами, в Python имена привязаны к значениям . Присвоение нового значения имени не влияет на старое значение (если больше нет ссылок на старое значение, и в этом случае (в CPython) он будет собирать мусор.)
unutbu
1
safegetМетод также не будет в случае , если ключ вложенной Словаре существует, но значение равно нулю. Будет добавлена TypeError: 'NoneType' object is not subscriptableследующая итерация
Стэнли Ф.
63

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

def deep_get(dictionary, *keys):
    return reduce(lambda d, key: d.get(key) if d else None, keys, dictionary)
Йоав Т
источник
5
Просто хотел упомянуть, что functools больше не встроен в Python3 и должен быть импортирован из functools, что делает этот подход немного менее элегантным.
yoniLavi
5
Небольшая поправка к этому комментарию: сокращение больше не является встроенным в Py3. Но я не понимаю, почему это делает его менее элегантным. Это делает его менее подходящим для однострочного текста, но однострочность не означает, что он автоматически квалифицируется или дисквалифицируется как «элегантный».
PaulMcG
35

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

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
Томас
Это хорошее решение, хотя есть и недостаток: даже если первый ключ недоступен или значение, переданное в качестве аргумента словаря функции, не является словарем, функция перейдет от первого элемента к последнему. По сути, так происходит во всех случаях.
Арсений
1
deep_get({'a': 1}, "a.b")дает, Noneно я ожидал бы исключения вроде KeyErrorили чего-то еще.
stackunderflow
@edityouprofile. тогда вам просто нужно сделать небольшое изменение, чтобы изменить возвращаемое значение с NoneнаRaise KeyError
Юда Правира
16

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

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)
Питикос
источник
9

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

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')
zzz
источник
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 спасибо! Я скоро опубликую новые функции, следите за обновлениями 😉
Фабио Каккамо
@ perfecto25 Я добавил поддержку индексов списков, например. d.get('a.b[0].c[-1]')
Фабио Каккамо,
Функция from_toml, похоже, не реализована. И может быть сложно импортировать BeneDict.
DLyons
@DLyons, вы ошибаетесь, в любом случае не стесняйтесь открывать вопрос на GitHub.
Фабио Каккамо
1
Да, все в порядке. Жалко, что я пропустил это - сэкономило бы мне время. Бенедикт, кажется, обладает некоторыми очень полезными функциями.
DLyons
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')
Джейкоб CUI
источник
2

Увидев это для глубокого получения атрибутов, я сделал следующее, чтобы безопасно получать вложенные 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)
Донни Уинстон
источник
7
fallbackфактически не используется в функции.
153957,
Обратите внимание, что это не работает для ключей, содержащих.
JW.
Когда мы вызываем obj [name], почему бы не obj.get (name, fallback) и избегать try-catch (если вы действительно хотите try-catch, тогда возвращайте резервный вариант, а не None)
denvar
Спасибо @ 153957. Я починил это. И да, @JW, это работает для моего варианта использования. Вы можете добавить sep=','ключевое слово arg для обобщения для заданных (sep, fallback) условий. И @denvar, если objэто говорит о типе intпосле последовательности сокращения, тогда obj [name] вызывает TypeError, которую я улавливаю. Если бы я использовал вместо этого obj.get (name) или obj.get (name, fallback), это вызвало бы 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', ''))

(Правда, 'холла')

(Ложь, '')

penduDev
источник
1

Уже есть много хороших ответов, но я придумал функцию под названием get, похожую на lodash get в области JavaScript, которая также поддерживает доступ к спискам по индексу:

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
Питер Марклунд
источник
единственный, который проходит мои тесты со списками :)
Игорь Леонидович
0

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

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

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

GenesRus
источник
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

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

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

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

Адам Хаусман
источник
0

Для вложенного словаря / поиска JSON вы можете использовать диктор

pip install dictor

объект 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

perfecto25
источник
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)
Darex1991
источник
не работает: test_dict = {"a": {"b": [{"c": "value"}]}} self.assertEqual (safeget (test_dict, ["a", "b", 1, "c "], Нет)
Игорь Л.