У мене вкладений словник. Чи є лише один спосіб безпечно вивести значення?
try:
example_dict['key1']['key2']
except KeyError:
pass
А може, python має такий метод, як get()
для вкладеного словника?
except keyerror:
пункті.
У мене вкладений словник. Чи є лише один спосіб безпечно вивести значення?
try:
example_dict['key1']['key2']
except KeyError:
pass
А може, python має такий метод, як get()
для вкладеного словника?
except keyerror:
пункті.
Відповіді:
Ви можете використовувати 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')
safeget
Метод у багатьох відносинах не дуже безпечних , так як він перезаписує вихідний словник, тобто ви не можете спокійно робити такі речі safeget(dct, 'a', 'b') or safeget(dct, 'a')
.
dct = dct[key]
переназначає нове значення для локальної змінної dct
. Це не мутує оригінальний дікт (тому на оригінальний дік не впливає safeget
). Якщо, з іншого боку, dct[key] = ...
він був використаний, то початковий дикт був би змінений. Іншими словами, імена Python пов'язані зі значеннями . Присвоєння нового значення імені не впливає на старе значення (якщо тільки немає більше посилань на старе значення, в цьому випадку (у CPython) воно буде збирати сміття.)
safeget
Метод також не буде в разі , якщо ключ вкладеної Словнику існує, але значення дорівнює нулю. Це кине TypeError: 'NoneType' object is not subscriptable
наступну ітерацію
Ви також можете використати пітон зменшити :
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 :
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 ''
Побачивши це для глибокого отримання атрибутів, я зробив наступне, щоб безпечно отримати вкладені 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)
fallback
насправді не використовується у функції.
.
sep=','
аргумент ключового слова для узагальнення за заданими умовами (sep, backback). І @denvar, якщо obj
говорять про тип int
після послідовності зменшення, тоді obj [ім'я] викликає TypeError, який я вловлю. Якщо я використовував obj.get (ім'я) або obj.get (ім'я, запасний) замість цього, це призвело б до виникнення AttributeError, тому будь-який спосіб мені потрібно впіймати.
Ще одна функція для тієї ж речі також повертає булеве значення, яке відображає, знайдено ключ чи ні, і обробляє деякі несподівані помилки.
'''
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')
(Помилковий, '')
Ви можете використовувати pydash:
import pydash as _
_.get(example_dict, 'key1.key2', default='Default')
Адаптація відповіді 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)
Я використовував рішення, яке схоже на подвійний get, але з додатковою здатністю уникати TypeError, використовуючи, якщо інша логіка:
value = example_dict['key1']['key2'] if example_dict.get('key1') and example_dict['key1'].get('key2') else default_value
Однак чим більше вкладений словник, тим більш громіздким він стає.
Для вкладених словників / пошуку 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']
Ви можете надати резервне значення у випадку, якщо ключ не на шляху
є багато інших варіантів, як-от ігнорувати обробку літер та використовувати інші символи, крім '.' як роздільник шляху,
Я трохи змінив цю відповідь. Я додав перевірку, чи використовуємо ми список із цифрами. Тож тепер ми можемо використовувати його будь-яким способом. 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)
Вже є багато хороших відповідей, але я придумав функцію під назвою отримати схожий на 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