检查嵌套字典中的键是否存在

8
我正在处理与API调用相关的内容,因此需要使用Python字典进行操作。
然而,对于同一请求,我并不总是使用相同的键,并且我想知道何时可以在没有异常的情况下调用一个键...
假设我有以下内容:
test = {'a':{'b':{'c':{'d':'e'}}}}

有时候键 d 存在,有时候不存在。有时候 c 甚至都不存在。
我想以某种方式检查 test['a']['b']['c']['d'] 是否存在,并且只使用一行代码。
我所尝试的:
1. 使用 test.get('a', {}).get('b', {}).get('c', {}).get('d', {})。它运行良好,但是很混乱,有时我有5-6个嵌套字典,名字非常长……
2. 使用 try/except 块非常好,但通常如果 test['a']['b']['c']['d'] 不存在,我将尝试调用 test['a']['b']['e']['f'] 来检查是否存在,因此我需要为我的每个 if 语句添加 try/catch,如果我没错的话,如果捕获到异常,则不再执行 try 块。
我也许试图寻找某种反射方式来做到这一点,使用“对象”名称作为字符串调用一个函数,该函数将检查每个键是否存在,如果存在,则返回对象本身。
有什么想法吗?
其背后的用途将是省略无用的情况,并假设有时信息在 test['a']['b']['c']['d'] 中,有时在 test['a']['b']['f'] 中:
if test['a']['b']['c']['d'] **exists**:
    do sthg with the value of test['a']['b']['c']['d']
elif test['a']['b']['f'] **exists**:
    do sthg else with the value of test['a']['b']['f']
else:
    do sthg different

如果我在那里放置一个try/except,第一个异常会停止执行并不允许我执行elif吗?
此外,我真的很喜欢调用test['a']['b']['c']['d']的方式,比提供键列表更好。实际上,我希望它对我和将阅读/使用我的代码的人尽可能透明。

你的代码需要做什么?似乎try/except块是正确的方法。你想要例如它失败的深度吗? - remram
@remram 如果键d不存在,我的信息可能会在另一个键中。 如果 test['a']['b']['c']['d'] 存在: 做某事 否则,如果 test['a']['b']['e'] 存在: 做某事 否则: 做某事 如果我没错的话,如果 test['a']['b']['c']['d'] 不存在,并且所有内容都在try/catch中,elif 部分将不会被执行,对吗? - user2497262
1
这类似于通过键列表访问Python嵌套字典项 - Cristian Ciupitu
@cristian-ciupitu 不是很对。在另一个问题中,OP想要通过键的列表访问值,假设该键存在。 老实说,我甚至更喜欢正常调用test['a']['b']['c']的方式,而不是提供一个键的列表,目前所有的解决方案都在使用 :/ - user2497262
1
实际上,我相当喜欢你使用.get(key,{})的原始方法,比答案更好。 - WestCoastProjects
6个回答

3

您可以编写一个递归函数来进行检查:

def f(d, keys):
    if not keys:
        return True
    return keys[0] in d and f(d[keys[0]], keys[1:])

如果函数返回True,则表示这些键存在:
In [10]: f(test,"abcd")
Out[10]: True

In [11]: f(test,"abce")
Out[11]: False

如果你想测试多个键的组合:

for keys in ("abce","abcr","abcd"):
    if f(test,keys):
        print(keys)
        break
abcd

要返回值很简单:
def f(d, keys):
    if len(keys) == 1:
         return d[keys[0]] if keys[0] in d else False
    return keys[0] in d and f(d[keys[0]], keys[1:])

print(f(test,"abcd"))
e

您可以再次测试多个键组合:

def test_keys(keys):
    for keys in keys:
        val = f(test,keys)
        if val:
            return val
    return False


print(test_keys(("abce","abcr","abc")))

您可以通过迭代方式编写该函数:
def f(d, keys):
    obj = object
    for k in keys:
        d = d.get(k, obj)
        if d is obj:
            return False
    return d

print(f(test,"abcd"))
e

如果您想根据返回值运行条件:
def f(d, keys):
    obj = object
    for k in keys:
        d = d.get(k, obj)
        if d is obj:
            return False
    return d

from operator import mul

my_actions = {"c": mul(2, 2), "d": lambda: mul(3, 3), "e": lambda: mul(3, 3)}

for st in ("abce", "abcd", "abcf"):
    val = f(test, st)
    if val:
        print(my_actions[val]())
9

只需按照与 if/elif 语句相同的顺序测试键组合即可。


3

虽然它不检查存在性,但这里有一个类似于 dict.get 方法的一行代码:

In [1]: test = {'a':{'b':{'c':{'d':'e'}}}}
In [2]: keys = 'abcd' # or ['a', 'b', 'c', 'd']

In [3]: reduce(lambda d, k: d.get(k) if d else None, keys, test)
Out[3]: 'e'

In [4]: keys = 'abcf'

In [5]: reduce(lambda d, k: d.get(k) if d else None, keys, test)

不幸的是,这种方法并不十分高效,因为它只有在所有键都存在时才会停止。


问题不在于如果键丢失就停止 - 我只是不想在键丢失时出现异常,以便能够评估下一个可能的键。 - user2497262
@user2497262,正如我的演示所示,没有引发任何异常。 - Cristian Ciupitu

0
我会创建一个递归函数。
def get_key(d, *args):
    if not args:
        return None
    val = d.get(args[0], None)

    if len(args) == 1:
        return val
    if isinstance(val, dict):
        return get_key(val, *args[1:])

0

您可以嵌套try块以处理两种类型的缺少关键字的异常。依赖try块符合Python中EAFP(容易请求宽恕比获得许可容易)哲学,而不是在使用前测试存在性之后进行跳转(LBYL)模式。 在多线程程序中,它还有助于避免另一个线程在测试存在性和使用值之间修改test字典引起的意外TOCTTOU(检查时间到使用时间)行为。

try:
    value_abcd = test['a']['b']['c']['d']
except KeyError:
    try:
        value_abf = test['a']['b']['f']
    except KeyError:
        print("do something different")
    else:
        print("value_abf is", value_abf)
else:
    print("value_abcd is", value_abcd)

我已经注意到你有远超过两个键。嵌套这么多键的尝试块会创建一个箭头反模式。所以,相反地,你可以尝试以下构造,在相同缩进级别处理所有键,只要访问发生在函数或for循环中,以便它可以returncontinue。如果不是,则提取方法

try:
    value_abcd = test['a']['b']['c']['d']
except KeyError:
    pass
else:
    print("value_abcd is", value_abcd)
    return  # or continue if doing this in a loop

try:
    value_abf = test['a']['b']['f']
except KeyError:
    pass
else:
    print("value_abf is", value_abf)
    return

print("do something different")

如果操作者有20个条件,那么会出现什么情况呢?会有20个嵌套的try/except吗? - Padraic Cunningham
@user2497262 我编辑了一个替代方案,可以处理多个键而不必使箭头。 - Damian Yerrick

0
如果您正在使用JSON,可以编写一个简单的类与导入的dict一起使用。
给定以下JSON代码片段:
>>> js='{"a": {"b": {"c": {"d": "e"}}}}'

如果由对象对组成,通常会解码为Python字典:

>>> import json
>>> json.loads(js)
{u'a': {u'b': {u'c': {u'd': u'e'}}}}

作为普通的Python字典,它可能会遇到缺失键的KeyError。你可以使用__missing__钩子来覆盖KeyErrors并达到你的原始结构:
class Mdict(dict):
    def __missing__(self, key):
        return False

现在进行测试:

>>> md=Mdict({'a':Mdict({'b':Mdict({'c':Mdict({'d':'e'})})})})
>>> if md['a']['b']['d']:
...    print md['a']['b']['d']
... elif md['a']['b']['c']:
...    print 'elif', md['a']['b']['c']  
... 
elif {'d': 'e'}

字典的每个级别都需要是一个Mdict,而不是普通的Python dict。然而,如果您正在使用JSON,则可以轻松实现这一点。只需在解码JSON时应用object_pairs_hook即可:

>>> js
'{"a": {"b": {"c": {"d": "e"}}}}'
>>> md=json.loads(js, object_pairs_hook=Mdict)

这将应用类 Mdict 而不是默认的 Python dict 来解码 JSON。

>>> md
{u'a': {u'b': {u'c': {u'd': u'e'}}}}
>>> md['a']
{u'b': {u'c': {u'd': u'e'}}}
>>> md['a']['c']
False

这里的其余示例保持不变。


我喜欢这种想法。不过,这意味着我需要将我的JSON输出转换为自己的字典,是吗? - user2497262
1
您可以在JSON编码器中使用object_pairs_hook来将其应用为导入。 - dawg
很好,这正是我在寻找的!让我像以前一样调用该字典,而不需要调用其他函数进行检查。完美:D - user2497262
1
请记住,在JSON字典中,False也是一个合法的值。您可能希望__missing__返回一个在其他情况下不可能解码的值,例如object(),以避免歧义。 - dawg

0

另一个为了完整性,但只递归检查一个键:

def hasKey(d, key):
    found = False
    if isinstance(d, dict):
        for k in d:
            found = True if k == key else found or hasKey(d[k], key)
    if isinstance(d, list):
        for i in d:
            found = found or hasKey(i, key)
    return found

检查嵌套字典 map 中是否存在键 key


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