获取嵌套字典值的安全方法

290

我有一个嵌套的字典。是否只有一种安全地获取其中值的方法?

try:
    example_dict['key1']['key2']
except KeyError:
    pass

或者 Python 中是否有像 get() 方法一样可以访问嵌套字典的方法?


1
参见:https://dev59.com/MWUq5IYBdhLWcg3wNtyB - dreftymac
1
在我看来,你问题中的代码已经是从字典中获取嵌套值的最佳方式了。你可以在 except KeyError: 子句中指定默认值。 - Peter Schorn
请注意,这个方法无法处理特殊情况 example_dict = {'key1': None}(会导致 TypeError 错误,或者如果使用了 get('key1', {}),则会导致 AttributeError 错误)。请参考这个答案了解更多信息。 - undefined
34个回答

553

你可以使用get两次:

example_dict.get('key1', {}).get('key2')

如果key1key2不存在,这将返回None

请注意,如果example_dict['key1']存在但不是字典(或具有get方法的类似字典的对象),则仍可能引发AttributeError。如果example_dict['key1']无法进行下标操作,则您发布的try...except代码将引发TypeError

另一个区别是,try...except在第一个丢失的键后立即短路。一系列的get调用则没有。


如果您希望保留语法example_dict['key1']['key2'],但不希望它引发KeyErrors,那么您可以使用Hasher recipe

class Hasher(dict):
    # https://dev59.com/WXA75IYBdhLWcg3wPmZ8#3405143
    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。

由于Hasherdict的子类,您可以像使用dict一样使用Hasher。所有相同的方法和语法都可用,只是Hasher对待缺失的键有所不同。

您可以像这样将常规的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')

97
那么,Python对于这种情况没有优美的解决方案吗?:( - Arti
3
safeget 方法在很多方面并不安全,因为它会覆盖原始的字典,这意味着您无法安全地执行诸如 safeget(dct, 'a', 'b') 或 safeget(dct, 'a') 这样的操作。 - neverfox
8
dct = dct[key] 将一个新值重新赋给了局部变量 dct。这不会改变原始字典(因此原始字典不受 safeget 的影响)。如果使用 dct[key] = ...,则会修改原始字典。换句话说,在Python中,名称绑定到值。将新值分配给名称不会影响旧值(除非旧值没有更多的引用,在这种情况下(在CPython中)它将被垃圾回收)。 - unutbu
3
safeget 方法在嵌套字典中的键存在但值为 null 时也会失败。下一次迭代会抛出 TypeError: 'NoneType' object is not subscriptable - Stanley F.
4
从Python 3.4开始,您可以使用with suppress(KeyError):。请参见此答案:https://dev59.com/aG7Xa4cB1Zd3GeqPuNJ9#45874251 - IODEV
显示剩余6条评论

86

通过结合这里提供的所有答案和我所做的一些小改变,我认为这个函数会很有用。它是安全的、快速的、易于维护的。

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

1
非常适合Jinja2模板。 - Thomas
这是一个不错的解决方案,但也有一个缺点:即使第一个键不可用,或者作为函数字典参数传递的值不是字典,该函数将从第一个元素到最后一个元素进行。基本上,在所有情况下都是这样做的。 - Arseny
1
deep_get({'a': 1}, "a.b") 返回 None,但我期望会抛出 KeyError 或其他异常。 - Saddle Point
3
@edityouprofile。然后你只需要进行小修改,将返回值从“None”更改为“Raise KeyError”。 - Yuda Prawira
只是提供信息,不处理嵌套列表。也就是说,这个不会起作用 deep_get(person, 'lastNames.0.name') - blisstdev

81

你也可以使用Python的reduce函数:

def deep_get(dictionary, *keys):
    return reduce(lambda d, key: d.get(key) if d else None, keys, dictionary)

11
想提醒一下,functools在Python3中已经不再是内置函数了,需要从functools模块中导入,这使得这种方法稍微不太优雅了一些。 - yoniLavi
11
对这条评论进行轻微修改:在Py3中,_reduce_不再是内置函数。但我不认为这会使它变得不够优雅。这确实使它不太适合作为一行代码,但成为一行代码并不自动具备或排除某个东西的“优雅性”。 - PaulMcG
1
请注意,通常使用try/except更好,因为它不涉及检查键的有效性的额外计算负担。因此,如果大多数时间键存在,则建议使用try/except范例以提高效率。 - milembar

30
您可以在第一阶段中获得一个空字典。
example_dict.get('key1',{}).get('key2')

18

基于Yoav的回答,还有更安全的方法:

def deep_get(dictionary, *keys):
    return reduce(lambda d, key: d.get(key, None) if isinstance(d, dict) else None, keys, dictionary)

17

一个递归的解决方案。它不是最有效的,但我发现它比其他示例更易读,并且不依赖于functools。

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)

16

我建议你尝试使用python-benedict

它是一个dict子类,提供了 keypath 支持和更多功能。

安装方法:pip install python-benedict

from benedict import benedict

example_dict = benedict(example_dict, keypath_separator='.')

现在你可以使用键路径访问嵌套值:

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上得到了良好的测试并且是开源的:

https://github.com/fabiocaccamo/python-benedict

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



@perfecto25 谢谢!我很快会发布新功能,请保持关注。 - Fabio Caccamo
@perfecto25 我添加了对列表索引的支持,例如 d.get('a.b[0].c[-1]') - Fabio Caccamo
from_toml函数似乎没有被实现。而且导入BeneDict可能会很困难。 - DLyons
@DLyons,你错了,无论如何,欢迎在GitHub上开一个问题。 - Fabio Caccamo
1
是的,它确实存在。可惜我错过了它——本来可以节省我一些时间的。Benedict似乎有一些非常有用的功能。 - DLyons
显示剩余3条评论

10

glom 是一个很棒的库,可以进行点查询:

In [1]: from glom import glom

In [2]: data = {'a': {'b': {'c': 'd'}}}

In [3]: glom(data, "a.b.c")
Out[3]: 'd'

一个查询失败会生成漂亮的堆栈跟踪,指示出精确的失败位置:
In [4]: glom(data, "a.b.foo")
---------------------------------------------------------------------------
PathAccessError                           Traceback (most recent call last)
<ipython-input-4-2a3467493ac4> in <module>
----> 1 glom(data, "a.b.foo")

~/.cache/pypoetry/virtualenvs/neural-knapsack-dE7ihQtM-py3.8/lib/python3.8/site-packages/glom/core.py in glom(target, spec, **kwargs)
   2179 
   2180     if err:
-> 2181         raise err
   2182     return ret
   2183 

PathAccessError: error raised while processing, details below.
 Target-spec trace (most recent last):
 - Target: {'a': {'b': {'c': 'd'}}}
 - Spec: 'a.b.foo'
glom.core.PathAccessError: could not access 'foo', part 2 of Path('a', 'b', 'foo'), got error: KeyError('foo')

使用 default 保护:

In [5]: glom(data, "a.b.foo", default="spam")
Out[5]: 'spam'
glom 的优美之处在于其灵活的规范参数。例如,可以轻松从以下 data 中提取所有的名字:
In [8]: data = {
   ...:     "people": [
   ...:         {"first_name": "Alice", "last_name": "Adams"},
   ...:         {"first_name": "Bob", "last_name": "Barker"}
   ...:     ]
   ...: }

In [9]: glom(data, ("people", ["first_name"]))
Out[9]: ['Alice', 'Bob']

阅读glom文档以获取更多示例。


这太棒了,正是我需要的动态XML解析工具。 - Oras

10

2
在我看来,这必须是被接受的答案! - Nam G VU

10

虽然 reduce 方法简洁明了,但我认为使用循环更容易理解。我还包含了一个默认参数。

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

作为理解如何使用一行reduce的练习,我做了以下操作。但最终循环方法对我来说似乎更直观。

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

我喜欢循环,因为它更加灵活。例如,可以使用它在嵌套字段上应用一些lambda,而不是获取它。 - milembar

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