在字典中,检查嵌套键是否存在的优雅方法是什么?

154

有没有更容易阅读的方法来检查字典中是否存在一个键,而不是独立地检查每个级别?

假设我需要获取一个嵌套对象中的值(例如来自维基数据的示例):

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

20个回答

225

简单说,使用Python编程时,你应该相信它的 宁愿请求原谅,而不是事先获准 的哲学思想。

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

答案

以下是我处理嵌套字典键的方法:

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

给定一个元素,循环按照给定顺序测试每个键。

我更喜欢这种方法,而不是我找到的所有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
3
很好的回答,但有一件事应该改变:将 if type(element) is not dict 改为 if not isinstance(element, dict)。这样它将适用于像 OrderedDict 这样的类型。 - Fonic
显示剩余9条评论

31

您可以使用默认值的 .get 方法:

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

但是这几乎肯定比使用try/except不清晰。


1
无论您将最后一个“get”作为默认值提供什么,它都可能恰好是s['mainsnak']['datavalue']['value']['numeric-id']的值。 - timgeb
13
我经常使用这个结构,但最近却被它“射了一脚”。在使用上面的例子时要小心,因为如果“getted”元素实际存在且不是字典(或者是可以调用get方法的对象)(在我的情况中是None),就会出现“'NoneType' object has no attribute 'get'”或类似的错误。请谨慎使用。 - darkless

17

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")

语法错误:无效语法 在 if key_exists := dictionary.get("key_1", {}).get("key_2"): 处 - aysh
@aysh 这是 Python 3.8 的示例。 - Lucas Vazquez
1
如果dictionary['main_key']['sub_key'] == False怎么办?当key不存在时,您需要显式检查get返回的sentinel值,而不是假定None是唯一的falsey值。 - chepner
@chepner 是的,那是一个非常好的观点。我修改了我的答案。 - Lucas Vazquez
是否可以为键值添加类型?例如: if sub_key_value := dictionary.get("main_key", {}).get("sub_key") -> List[str]: - alexwatever
你好 @alexwatever,几个小时前我添加了一条评论,但现在我删除了它,想要评论其他的东西。我给你留下一个 Pastebin 链接,里面有三种不同的方法来为这个句子添加类型。祝你愉快!链接:https://pastebin.com/EFVQMyUu - Lucas Vazquez

14

尝试/捕获似乎是实现此目的最具Python风格的方法。
以下递归函数应该可行(如果在字典中没有找到一个键,则返回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

1
如果'value'是一个'numeric-ids'数组,你会怎么做呢? result = exists(myDict, ['mainsnak', 'datavalue', 'value[0]', 'numeric-id']) ? - Dss
@Maurice Meyer:如果存在“mainsnak2”,“mainsnak3”等(例如“mainsnak”,内部字典保持不变),那怎么办?在这种情况下,我们可以检查所有“mainsnak”,“mainsnak2”和“mainsnak3”中是否存在“datavalue”吗? - StackGuru
1
如果 numeric-idNone,我们无法确定该值是 None 还是键缺失。https://dev59.com/d1cQ5IYBdhLWcg3wA_BA#43491315 更好。 - srs

11

我建议您使用python-benedict,这是一个可靠的Python字典子类,具有完整的键路径支持和许多实用程序方法。

您只需将现有的字典强制转换即可:

s = benedict(s)

现在您的字典支持完整的键路径,并且您可以使用Pythonic方式检查键是否存在,使用 in 运算符:

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

这里是库的存储库和文档:https://github.com/fabiocaccamo/python-benedict

注意:我是该项目的作者


这是一个很棒的库,但经常会与BeneDict发生名称冲突。在我的环境中它根本无法使用,所以我不得不寻找替代方案。 - Andreas
该模块已在 PyPI 上注册为 python-benedict。 可能你的 IDE 假设要安装的包的名称与你正在导入的模块的名称相匹配,但这是错误的。我建议你完全掌控自己的操作,并手动安装所需的依赖 :) - Fabio Caccamo
@FabioCaccamo 感谢您的回复。如果可能的话,您能否请列举一些您的代码库相对于 @Alexander 推荐的 pydash 的优缺点呢?(主要是在性能/内存方面) - Michel Gokan Khan
@MichelGokanKhan,坦白说我不知道/使用pydash,所以我不能说,但如果你尝试了两者,请告诉我! - Fabio Caccamo

6

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 不是字符串、浮点数或其他类型的数据。 - Jordan Simba

4
该回答被接受,是一个很好的回答,但这里有另一种方法。如果你最终不得不经常这样做,它会少打一点字并且更容易看(在我看来)。与其他答案不同,它也不需要任何额外的包依赖。未进行性能比较。
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

这需要键是字符串,对吗? - Manu
嘿@Manu,你是正确的。支持非字符串是一个非常容易的更改。我会进行编辑。 - totalhack

3
所选答案在正常情况下可以工作,但我发现有几个明显的问题。如果您搜索 ["spam", "egg", "bacon", "pizza"],由于尝试使用字符串 "pizza" 索引 "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 except可能成为我们过于依赖的技巧。既然我已经需要检查类型,那么干脆就不要使用try except吧。 解决方案:
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..."

2

有点丑陋,但是实现这个最简单的方法就是一行代码

d = {
     'mainsnak': {
             'datavalue': {
                     'value': {
                             'numeric-id': {
                              }
                      }
              }
     }
}

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接