Елегантний спосіб перевірити, чи існує вкладений ключ у дикт?


85

Чи є більш зрозумілий спосіб перевірити, чи існує ключ, закопаний у дикт, без перевірки кожного рівня самостійно?

Скажімо, мені потрібно отримати це значення в захороненому об’єкті (приклад взято з Вікіданих):

x = s['mainsnak']['datavalue']['value']['numeric-id']

Щоб переконатися, що це не закінчується помилкою виконання, необхідно або перевірити кожен рівень так:

if 'mainsnak' in s and 'datavalue' in s['mainsnak'] and 'value' in s['mainsnak']['datavalue'] and 'nurmeric-id' in s['mainsnak']['datavalue']['value']:
    x = s['mainsnak']['datavalue']['value']['numeric-id']

Інший спосіб, яким я можу подумати, щоб вирішити це, - це обернути це в try catchконструкцію, яка, на мою думку, також досить незручна для такого простого завдання.

Я шукаю щось на зразок:

x = exists(s['mainsnak']['datavalue']['value']['numeric-id'])

який повертається, Trueякщо існують усі рівні.

Відповіді:


133

Якщо коротко, то з Python потрібно довіряти, що простіше просити прощення, ніж дозволу

try:
    x = s['mainsnak']['datavalue']['value']['numeric-id']
except KeyError:
    pass

Відповідь

Ось як я маю справу з вкладеними ключами dict:

def keys_exists(element, *keys):
    '''
    Check if *keys (nested) exists in `element` (dict).
    '''
    if not isinstance(element, dict):
        raise AttributeError('keys_exists() expects dict as first argument.')
    if len(keys) == 0:
        raise AttributeError('keys_exists() expects at least two arguments, one given.')

    _element = element
    for key in keys:
        try:
            _element = _element[key]
        except KeyError:
            return False
    return True

Приклад:

data = {
    "spam": {
        "egg": {
            "bacon": "Well..",
            "sausages": "Spam egg sausages and spam",
            "spam": "does not have much spam in it"
        }
    }
}

print 'spam (exists): {}'.format(keys_exists(data, "spam"))
print 'spam > bacon (do not exists): {}'.format(keys_exists(data, "spam", "bacon"))
print 'spam > egg (exists): {}'.format(keys_exists(data, "spam", "egg"))
print 'spam > egg > bacon (exists): {}'.format(keys_exists(data, "spam", "egg", "bacon"))

Вихід:

spam (exists): True
spam > bacon (do not exists): False
spam > egg (exists): True
spam > egg > bacon (exists): True

Це цикл в заданому elementтестуванні кожного ключа в заданому порядку.

Я віддаю перевагу цьому всім variable.get('key', {})методам, які знайшов, оскільки це відповідає EAFP .

Функція крім називатися як: keys_exists(dict_element_to_test, 'key_level_0', 'key_level_1', 'key_level_n', ..). Потрібні принаймні два аргументи, елемент та один ключ, але ви можете додати скільки ключів ви хочете.

Якщо вам потрібно використовувати якусь карту, ви можете зробити щось на зразок:

expected_keys = ['spam', 'egg', 'bacon']
keys_exists(data, *expected_keys)

Так, як уже зазначалося, це дійсне рішення. Але уявіть собі функцію, яка отримує доступ приблизно у 10 разів до такої змінної, всі try exceptтвердження залишать досить суцільний.
loomi

@loomi Ви можете зробити невелику функцію цією try-exceptлогікою і просто викликати це кожен раз
Chris_Rands

@loomi обертає його у функцію.
juanpa.arrivillaga

1
"У двох словах, з Python, якому ви повинні довіряти, простіше просити прощення, ніж дозволу" використовує набагато більше двох слів.
user2357112 підтримує Моніку

1
Великий відповідь, але одна річ повинна бути змінена: if type(element) is not dictв if not isinstance(element, dict). Таким чином, це буде працювати і для таких типів, як OrderedDict.
Maxxim

17

Ви можете використовувати .getіз типовими значеннями:

s.get('mainsnak', {}).get('datavalue', {}).get('value', {}).get('numeric-id')

але це майже напевно менш зрозуміло, ніж використання спроби / крім.


1
І що б ви не вказали останнє getяк значення за замовчуванням, це може просто бути значенням s['mainsnak']['datavalue']['value']['numeric-id'].
timgeb

4
Я багато використовував цю конструкцію, і мені це просто вдарило. Будьте обережні, використовуючи приклад вище, тому що якщо елемент "getted" насправді існує, а не є dict (або об'єктом, за яким ви можете зателефонувати get) (None - це мій випадок), це закінчиться тим, 'NoneType' object has no attribute 'get'чи типом, який у вас там є.
безтемний

9

Спроба / крім видається найбільш пітонічним способом зробити це.
Наступна рекурсивна функція повинна працювати (повертає None, якщо один з ключів не був знайдений у дикті):

def exists(obj, chain):
    _key = chain.pop(0)
    if _key in obj:
        return exists(obj[_key], chain) if chain else obj[_key]

myDict ={
    'mainsnak': {
        'datavalue': {
            'value': {
                'numeric-id': 1
            }
        }
    }
}

result = exists(myDict, ['mainsnak', 'datavalue', 'value', 'numeric-id'])
print(result)
>>> 1

Як би ви це зробили для масивів, наприклад, якби "value" було масивом "numeric-ids" result = существует (myDict, ['mainsnak', 'datavalue', 'value [0]', 'numeric-id'] )?
Dss

@Maurice Meyer: Що робити, якщо існують 'mainsnak2', 'mainsnak3' тощо (наприклад, як 'mainsnak', внутрішній словник залишається незмінним). У такому випадку, чи можемо ми перевірити, чи існує 'datavalue' у всіх 'mainsnak', 'mainsnak2' & 'mainsnak3'?
StackGuru

5

Ви можете використовувати, pydashщоб перевірити, чи існує: http://pydash.readthedocs.io/en/latest/api.html#pydash.objects.has

Або отримайте значення (можна навіть встановити за замовчуванням - повертати, якщо воно не існує): http://pydash.readthedocs.io/en/latest/api.html#pydash.objects.has

Ось приклад:

>>> get({'a': {'b': {'c': [1, 2, 3, 4]}}}, 'a.b.c[1]')
2

5

Python 3.8 +

dictionary = {
    "main_key": {
        "sub_key": "value",
    },
}

if sub_key_value := dictionary.get("main_key", {}).get("sub_key"):
    print(f"The key 'sub_key' exists in dictionary[main_key] and it's value is {sub_key_value}")
else:
    print("Key 'sub_key' doesn't exists")

SyntaxError: неправильний синтаксис при if key_exists: = dictionary.get ("key_1", {}). Get ("key_2"):
aysh

@aysh Це приклад Python 3.8
Лукас Васкес

5

Я пропоную вам використовувати python-benedictтвердий підклас python dict із повною підтримкою шляху шляху та безліччю корисних методів.

Вам просто потрібно скласти свій існуючий дикт:

s = benedict(s)

Тепер ваш dict має повну підтримку шляху шляху, і ви можете перевірити, чи існує ключ пітонічним способом, використовуючи оператор in :

if 'mainsnak.datavalue.value.numeric-id' in s:
    # do stuff

Ось сховище бібліотеки та документація: https://github.com/fabiocaccamo/python-benedict

Примітка: Я автор цього проекту


4

Спроба / крім способу - найчистіша, жодного змагання. Однак це також вважається винятком у моїй IDE, що зупиняє виконання під час налагодження.

Більше того, мені не подобається використовувати винятки як оперативні вказівки в методі, що, по суті, відбувається з функцією try / catch.

Ось коротке рішення, яке не використовує рекурсію та підтримує значення за замовчуванням:

def chained_dict_lookup(lookup_dict, keys, default=None):
    _current_level = lookup_dict
    for key in keys:
        if key in _current_level:
            _current_level = _current_level[key]
        else:
            return default
    return _current_level

Мені подобається це рішення :) ... Тут лише примітка. в якийсь момент current_level[key]може вказувати на значення, а не на внутрішній дикт. Тому будь-хто, хто користується цим, подбайте про те, щоб перевірити, current_levelчи не є це рядок, чи плаваючий засіб чи щось інше.
Йорданія Сімба

2

У мене була та сама проблема, і нещодавно з’явилася бібліотека python:
https://pypi.org/project/dictor/
https://github.com/perfecto25/dictor

Отже, у вашому випадку:

from dictor import dictor

x = dictor(s, 'mainsnak.datavalue.value.numeric-id')

Особиста примітка:
Мені не подобається ім'я "диктор", оскільки воно не натякає на те, що воно насправді робить. Тому я використовую його як:

from dictor import dictor as extract
x = extract(s, 'mainsnak.datavalue.value.numeric-id')

Не вдалося придумати кращого імені, ніж extract. Не соромтеся коментувати, якщо ви придумали більш життєздатні імена. safe_get, robust_getне вважав себе придатним для моєї справи.


1

Я написав бібліотеку синтаксичного аналізу даних datakneadдля таких випадків, в основному тому, що мене розчарував JSON, який також повертає API Wikidata.

За допомогою цієї бібліотеки ви могли б зробити щось подібне

from dataknead import Knead

numid = Knead(s).query("mainsnak/datavalue/value/numeric-id").data()

if numid:
    # Do something with `numeric-id`

1

Якщо ви можете потерпіти тестування рядкового представлення шляху до об'єкта, тоді цей підхід може вам підійти:

def exists(str):
    try:
        eval(str)
        return True
    except:
        return False

exists("lst['sublist']['item']")

але в обсязі цієї функції "lst" не визначено
Dss

1

Інший спосіб:

def does_nested_key_exists(dictionary, nested_key):
    exists = nested_key in dictionary
    if not exists:
        for key, value in dictionary.items():
            if isinstance(value, dict):
                exists = exists or does_nested_key_exists(value, nested_key)
    return exists

що таке does_nested_key_exists (значення, вкладений_ключ) тут
aysh
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.