优雅的方法来检查嵌套的关键字是否存在于一个结果?

是否有更多可读的方法来检查是否有一个关键隐藏在一个字典存在,而不检查每一级独立?

假设我需要在一个隐藏的对象中得到这个值(例子取自 Wikidata) :

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

121701 次浏览

简而言之,对于 Python,您必须相信它是 请求原谅比请求许可更容易

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

答案

下面是我如何处理嵌套的 dictkey:

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)

你可以使用默认的 .get:

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

但是这几乎肯定不如使用 try/竹篮打水一场空。

“尝试/除外”似乎是做到这一点的最简单的方法。
下面的递归函数应该可以工作(如果在 dict 中没有找到某个键,则返回 Nothing) :

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

可以使用 pydash检查是否存在: http://pydash.readthedocs.io/en/latest/api.html#pydash.objects.has

或者获取值(如果不存在,甚至可以设置 default-to return) : http://pydash.readthedocs.io/en/latest/api.html#pydash.objects.has

这里有一个例子:

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

我为这种情况编写了一个名为 dataknead的数据解析库,主要是因为 Wikidata API 返回的 JSON 让我感到沮丧。

有了那个图书馆,你可以做这样的事情

from dataknead import Knead


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


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

尝试[除外]的方式是最干净的,没有竞争。但是,它也算作 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

如果您可以忍受测试对象路径的字符串表示,那么这种方法可能适合您:

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


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

我建议您使用 python-benedict,它是一个实体的 pythondictsubclass,支持完整的键盘路径和许多实用工具方法。

你只需要投出你现有的魔咒:

s = benedict(s)

现在你的 dict 已经有了完整的密钥路径支持,你可以检查密钥是否以 pythonic 的方式存在,使用 in 运算符:

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

这里是图书馆资料库和文档: Https://github.com/fabiocaccamo/python-benedict

注意: 我是这个项目的作者

我也有同样的问题,最近的 python lib 出现了:
Https://pypi.org/project/dictor/
Https://github.com/perfecto25/dictor

所以对你来说:

from dictor import dictor


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

个人提示:
我不喜欢“ dictor”这个名字,因为它没有暗示它实际上是做什么的:

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

没有比 extract更好的名字了。如果你想到更可行的命名方法,请随时发表评论。safe_getrobust_get都不适合我的案子。

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 or their value is Falsy")

号外

一点但很重要的澄清。

在前面的代码块中,我们验证字典中存在一个键,但是它的值也是 Truthy。 大多数时候,这才是人们真正想要的,我认为这才是 OP 真正想要的。但是,这并不是最“正确”的答案,因为如果键存在但其值为 False,上面的代码块将告诉我们键不存在,这是不正确的。

所以,我在这里给出一个更正确的答案:

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


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

另一种方式:

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

公认的 回答是一个很好的方法,但这里有另一种方法。在我看来,如果你最终不得不经常这样做,那么打字就会少一些,眼睛也会容易一些。它也不需要任何额外的包依赖关系,就像其他一些答案一样。没有比较性能。

import functools


def haskey(d, path):
try:
functools.reduce(lambda x, y: x[y], path.split("."), d)
return True
except KeyError:
return False


# Throwing in this approach for nested get for the heck of it...
def getkey(d, path, *default):
try:
return functools.reduce(lambda x, y: x[y], path.split("."), d)
except KeyError:
if default:
return default[0]
raise

用法:

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


(Pdb) haskey(data, "spam")
True
(Pdb) haskey(data, "spamw")
False
(Pdb) haskey(data, "spam.egg")
True
(Pdb) haskey(data, "spam.egg3")
False
(Pdb) haskey(data, "spam.egg.bacon")
True

这个问题答案的原创灵感。

编辑: 一条评论指出,这只适用于字符串键。一种更通用的方法是接受可迭代路径参数:

def haskey(d, path):
try:
functools.reduce(lambda x, y: x[y], path, d)
return True
except KeyError:
return False


(Pdb) haskey(data, ["spam", "egg"])
True

答案有很多。以下是我对此的拙见。增加了字典数组的检查。请注意,我没有检查参数的有效性。我用了上面阿诺特的部分代码。之所以添加这个答案,是因为我得到了一个需要检查数组或字典的用例。 密码如下:

def keys_exists(element, *keys):
'''
Check if *keys (nested) exists in `element` (dict).
'''
    

retval=False
if isinstance(element,dict):
for key,value in element.items():
for akey in keys:
if element.get(akey) is not None:
return True
if isinstance(value,dict) or isinstance(value,list):
retval= keys_exists(value, *keys)
            

elif isinstance(element, list):
for val in element:
if isinstance(val,dict) or isinstance(val,list):
retval=keys_exists(val, *keys)


return retval

使用带默认值的 dict 是简洁的,并且似乎比使用连续的 if 语句执行得更快。

你自己试试:

import timeit


timeit.timeit("'x' in {'a': {'x': {'y'}}}.get('a', {})")
# 0.2874350370002503


timeit.timeit("'a' in {'a': {'x': {'y'}}} and 'x' in {'a': {'x': {'y'}}}['a']")
# 0.3466246419993695


所选择的答案在快乐的道路上运行良好,但对我来说有几个显而易见的问题。如果你要搜索[“垃圾邮件”,“鸡蛋”,“培根”,“披萨”] ,它会抛出一个类型错误,因为试图索引“ well...”使用字符串“披萨”。比如,如果你把披萨换成2它就会用这个来得到“ Well...”的索引2

选择答案输出问题 :

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


print(keys_exists(data, "spam", "egg", "bacon", "pizza"))
>> TypeError: string indices must be integers


print(keys_exists(data, "spam", "egg", "bacon", 2)))
>> l

我还觉得使用 try but 可能会成为我们过于迅速依赖的拐杖。因为我相信我们已经需要检查类型,不妨删除尝试除外。

解决方案:

def dict_value_or_default(element, keys=[], default=Undefined):
'''
Check if keys (nested) exists in `element` (dict).
Returns value if last key exists, else returns default value
'''
if not isinstance(element, dict):
return default


_element = element
for key in keys:
# Necessary to ensure _element is not a different indexable type (list, string, etc).
# get() would have the same issue if that method name was implemented by a different object
if not isinstance(_element, dict) or key not in _element:
return default


_element = _element[key]
        

return _element

产出:

print(dict_value_or_default(data, ["spam", "egg", "bacon", "pizza"]))
>> INVALID


print(dict_value_or_default(data, ["spam", "egg", "bacon", 2]))
>> INVALID


print(dict_value_or_default(data, ["spam", "egg", "bacon"]))
>> "Well..."

下面是我根据@Aroust 的回答写的一小段话:

def exist(obj, *keys: str) -> bool:
_obj = obj
try:
for key in keys:
_obj = _obj[key]
except (KeyError, TypeError):
return False
return True


if __name__ == '__main__':
obj = {"mainsnak": {"datavalue": {"value": "A"}}}
answer = exist(obj, "mainsnak", "datavalue", "value", "B")
print(answer)

我添加了 TypeError,因为当 _obj为 str、 int、 Nothing 或 etc 时,它将引发该错误。

可以尝试使用它来检查 key/nestedkey/value 是否在嵌套 dict 中

import yaml


#d - nested dictionary
if something in yaml.dump(d, default_flow_style=False):
print(something, "is in", d)
else:
print(something, "is not in", d)

为此,我编写了一个方便的图书馆。

我正在迭代过去的结果,并试图检查是否有一个特定的关键是存在或没有。

看看这个。 Https://github.com/agent-hellboy/trace-dkey