是否有内置 dict.get() 的递归版本?

Is there a recursive version of the dict.get() built-in?

我有一个嵌套的字典对象,我希望能够检索具有任意深度的键的值。我可以通过子类化 dict:

来做到这一点
>>> class MyDict(dict):
...     def recursive_get(self, *args, **kwargs):
...         default = kwargs.get('default')
...         cursor = self
...         for a in args:
...             if cursor is default: break
...             cursor = cursor.get(a, default)
...         return cursor
... 
>>> d = MyDict(foo={'bar': 'baz'})
>>> d
{'foo': {'bar': 'baz'}}
>>> d.get('foo')
{'bar': 'baz'}
>>> d.recursive_get('foo')
{'bar': 'baz'}
>>> d.recursive_get('foo', 'bar')
'baz'
>>> d.recursive_get('bogus key', default='nonexistent key')
'nonexistent key'

但是,我不想必须继承 dict 才能获得此行为。是否有一些具有等效或相似行为的内置方法?如果没有,是否有提供此行为的任何标准或外部模块?

我目前正在使用 Python 2.7,不过我也很想知道 3.x 解决方案。

我知道 none。然而,你根本不需要子类化 dict,你可以只写一个函数,它接受一个字典、args 和 kwargs 并做同样的事情:

 def recursive_get(d, *args, **kwargs):
     default = kwargs.get('default')
     cursor = d
     for a in args:
         if cursor is default: break
         cursor = recursive_get(cursor, a, default)
     return cursor 

这样使用

recursive_get(d, 'foo', 'bar')

collections.default_dict 至少会为不存在的键提供默认值。

执行此操作的一种非常常见的模式是使用空字典作为默认值:

d.get('foo', {}).get('bar')

如果你有不止一对钥匙,你可以使用reduce(注意在Python3中必须导入reducefrom functools import reduce)来申请多次操作

reduce(lambda c, k: c.get(k, {}), ['foo', 'bar'], d)

当然,你应该考虑把它包装成一个函数(或方法):

def recursive_get(d, *keys):
    return reduce(lambda c, k: c.get(k, {}), keys, d)

考虑到默认关键字参数和元组分解的处理,您实际上可以在 Python 3 中非常巧妙地实现这一点:

In [1]: def recursive_get(d, *args, default=None):
   ...:     if not args:
   ...:         return d
   ...:     key, *args = args
   ...:     return recursive_get(d.get(key, default), *args, default=default)
   ...: 

类似的代码也适用于 python 2,但您需要恢复使用 **kwargs,就像您在示例中所做的那样。您还需要使用索引来分解 *args.

无论如何,如果您要使函数递归,则不需要循环。

您可以看到上面的代码演示了与您现有方法相同的功能:

In [2]: d = {'foo': {'bar': 'baz'}}

In [3]: recursive_get(d, 'foo')
Out[3]: {'bar': 'baz'}

In [4]: recursive_get(d, 'foo', 'bar')
Out[4]: 'baz'

In [5]: recursive_get(d, 'bogus key', default='nonexistent key')
Out[5]: 'nonexistent key'

您可以使用 defaultdict 为您提供有关缺少键的空字典:

from collections import defaultdict
mydict = defaultdict(dict)

这只深入一层 - mydict[missingkey] 是一个空字典,mydict[missingkey][missing key] 是一个 KeyError。您可以根据需要添加尽可能多的级别,方法是将其包装在更多 defaultdict 中,例如 defaultdict(defaultdict(dict))。您还可以将最里面的一个作为另一个 defaultdict,为您的用例提供一个合理的工厂函数,例如

mydict = defaultdict(defaultdict(lambda: 'big summer blowout'))

如果你需要它到达任意深度,你可以这样做:

def insanity():
    return defaultdict(insanity)

print(insanity()[0][0][0][0])

是正确的,但求助于 lambda 函数,只有在中间密钥 不是 [=41= 时才需要避免 TypeError ] 存在。如果这不是问题,您可以直接使用 dict.get

from functools import reduce

def get_from_dict(dataDict, mapList):
    """Iterate nested dictionary"""
    return reduce(dict.get, mapList, dataDict)

这是一个演示:

a = {'Alice': {'Car': {'Color': 'Blue'}}}  
path = ['Alice', 'Car', 'Color']
get_from_dict(a, path)  # 'Blue'

如果你希望比使用 lambda 更明确,同时仍然避免使用 TypeError,你可以包含一个 try / except 子句:

def get_from_dict(dataDict, mapList):
    """Iterate nested dictionary"""
    try:
        return reduce(dict.get, mapList, dataDict)
    except TypeError:
        return None  # or some other default value

最后,如果您希望在任何级别都不存在键时引发 KeyError,请使用 operator.getitemdict.__getitem__:

from functools import reduce
from operator import getitem

def getitem_from_dict(dataDict, mapList):
    """Iterate nested dictionary"""
    return reduce(getitem, mapList, dataDict)
    # or reduce(dict.__getitem__, mapList, dataDict)

请注意,[]__getitem__ 方法的语法糖。因此,这恰恰与您通常访问字典值的方式有关。 operator 模块只是提供了一种更易读的方法来访问此方法。

迭代解

def deep_get(d:dict, keys, default=None, create=True):
    if not keys:
        return default
    
    for key in keys[:-1]:
        if key in d:
            d = d[key]
        elif create:
            d[key] = {}
            d = d[key]
        else:
            return default
    
    key = keys[-1]
    
    if key in d:
        return d[key]
    elif create:
        d[key] = default
    
    return default


def deep_set(d:dict, keys, value, create=True):
    assert(keys)
    
    for key in keys[:-1]:
        if key in d:
            d = d[key]
        elif create:
            d[key] = {}
            d = d[key]
    
    d[keys[-1]] = value 
    return value

我将在 Django 项目中使用如下一行对其进行测试:

keys = ('options', 'style', 'body', 'name')

val = deep_set(d, keys, deep_get(s, keys, 'dotted'))