У меня есть вложенный словарь. Есть ли только один способ безопасно доставить ценности?
try:
example_dict['key1']['key2']
except KeyError:
pass
Или, может быть, у python есть такой метод, как get()
вложенный словарь?
У меня есть вложенный словарь. Есть ли только один способ безопасно доставить ценности?
try:
example_dict['key1']['key2']
except KeyError:
pass
Или, может быть, у python есть такой метод, как get()
вложенный словарь?
except keyerror:
.
Ответы:
Вы можете использовать get
дважды:
example_dict.get('key1', {}).get('key2')
Это вернется, None
если один из них key1
или key2
не существует.
Обратите внимание, что это все еще может вызвать AttributeError
if, 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')
safeget
метод во многих отношениях не очень безопасен, поскольку он перезаписывает исходный словарь, а это означает, что вы не можете безопасно делать такие вещи, как safeget(dct, 'a', 'b') or safeget(dct, 'a')
.
dct = dct[key]
переназначает новое значение локальной переменной dct
. Это не изменяет исходный dict (поэтому исходный dict не изменяется safeget
). Если бы, с другой стороны, dct[key] = ...
использовался, то исходный dict был бы изменен. Другими словами, в Python имена привязаны к значениям . Присвоение нового значения имени не влияет на старое значение (если больше нет ссылок на старое значение, и в этом случае (в CPython) он будет собирать мусор.)
safeget
Метод также не будет в случае , если ключ вложенной Словаре существует, но значение равно нулю. Будет добавлена TypeError: 'NoneType' object is not subscriptable
следующая итерация
Вы также можете использовать python reduce :
def deep_get(dictionary, *keys):
return reduce(lambda d, key: d.get(key) if d else None, keys, dictionary)
Я думаю, что эта функция будет полезна, объединив здесь все эти ответы и небольшие изменения, которые я внес. это безопасно, быстро, легко обслуживается.
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
>>>
deep_get({'a': 1}, "a.b")
дает, None
но я ожидал бы исключения вроде KeyError
или чего-то еще.
None
наRaise KeyError
Опираясь на ответ Йоава, можно сделать еще более безопасный подход:
def deep_get(dictionary, *keys):
return reduce(lambda d, key: d.get(key, None) if isinstance(d, dict) else None, keys, dictionary)
Рекурсивное решение. Он не самый эффективный, но я считаю его более читабельным, чем другие примеры, и он не полагается на функциональные модули.
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)
Хотя метод сокращения понятен и краток, я думаю, что простой цикл легче понять. Я также включил параметр по умолчанию.
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')
Предлагаю вам попробовать 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
Примечание: я являюсь автором этого проекта
d.get('a.b[0].c[-1]')
Простой класс, который может оборачивать словарный запас и извлекать его на основе ключа:
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 ''
для получения ключа второго уровня вы можете сделать это:
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)
fallback
фактически не используется в функции.
.
sep=','
ключевое слово arg для обобщения для заданных (sep, fallback) условий. И @denvar, если obj
это говорит о типе int
после последовательности сокращения, тогда obj [name] вызывает TypeError, которую я улавливаю. Если бы я использовал вместо этого obj.get (name) или obj.get (name, fallback), это вызвало бы AttributeError, так что в любом случае мне нужно было бы поймать.
Вы можете использовать pydash:
import pydash as _
_.get(example_dict, 'key1.key2', default='Default')
Еще одна функция для того же самого, также возвращает логическое значение, чтобы указать, был ли ключ найден или нет, и обрабатывает некоторые неожиданные ошибки.
'''
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', ''))
(Правда, 'холла')
(Ложь, '')
Уже есть много хороших ответов, но я придумал функцию под названием 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
Адаптация ответа unutbu, который я нашел полезным в моем собственном коде:
example_dict.setdefaut('key1', {}).get('key2')
Он генерирует словарную запись для key1, если у него еще нет этого ключа, чтобы вы избежали KeyError. Если вы хотите получить вложенный словарь, который все равно включает эту пару ключей, как это сделал я, это кажется самым простым решением.
Небольшое улучшение 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)
Решение, которое я использовал, похоже на двойное получение, но с дополнительной возможностью избежать TypeError с помощью логики if else:
value = example_dict['key1']['key2'] if example_dict.get('key1') and example_dict['key1'].get('key2') else default_value
Однако чем больше вложен словарь, тем громоздче он становится.
Для вложенного словаря / поиска 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']
вы можете предоставить резервное значение, если ключ отсутствует в пути
Вы можете использовать множество других параметров, например игнорировать регистр букв и использовать другие символы, кроме '.' как разделитель путей,
Я немного изменил этот ответ. Я добавил проверку, используем ли мы список с числами. Итак, теперь мы можем использовать его как угодно. 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)