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

Python 是否有内置dict.get()的递归版本?,python,dictionary,recursion,nested,Python,Dictionary,Recursion,Nested,我有一个嵌套的dictionary对象,我希望能够检索任意深度的键的值。我可以通过子类化dict来实现这一点: >>> class MyDict(dict): ... def recursive_get(self, *args, **kwargs): ... default = kwargs.get('default') ... cursor = self ... for a in args: ...

我有一个嵌套的dictionary对象,我希望能够检索任意深度的键的值。我可以通过子类化
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
。是否有一些内置方法具有相同或类似的行为?如果没有,是否有任何标准或外部模块提供这种行为


目前我正在使用Python2.7,不过我也很想听听3.x解决方案。

我还不知道。但是,您根本不需要将dict子类化,只需编写一个函数,该函数接受dictionary、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')

将至少为不存在的键处理默认值的提供。

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

d.get('foo', {}).get('bar')
如果有多个键,可以使用
reduce
(注意,在Python 3中,必须导入
reduce
from functools import reduce
)多次应用该操作

reduce(lambda c, k: c.get(k, {}), ['foo', 'bar'], d)
当然,你应该考虑把这个打包成一个函数(或方法):


考虑到Python 3对默认关键字参数和元组分解的处理,您实际上可以在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为缺少的密钥提供空dict:

from collections import defaultdict
mydict = defaultdict(dict)
这只会深入一层-
mydict[missingkey]
是一个空的dict,
mydict[missingkey][missingkey]
是一个键错误。您可以根据需要添加任意多个级别,方法是将其包装成更多的
defaultdict
s,例如
defaultdict(defaultdict(dict))
。您还可以将最里面的一个作为另一个defaultdict,并为您的用例提供一个合理的工厂函数,例如

mydict = defaultdict(defaultdict(lambda: 'big summer blowout'))
如果需要它进入任意深度,可以这样做:

def insanity():
    return defaultdict(insanity)

print(insanity()[0][0][0][0])
是正确的,但使用
lambda
函数,只有在中间密钥不存在时,才需要使用该函数来避免
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
/
中使用以下子句:

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.getitem
dict.。\uuuuu getitem\uuuuu

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)

请注意,
[]
\uuu getitem\uu
方法的语法糖。因此,这正是您通常访问字典值的方式。
操作符
模块只是提供了一种更可读的方法来访问此方法。

迭代解决方案

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

d、 get('foo')。get('bar')?听起来您对使用问题中发布的代码实现的功能相当满意。是否有任何特殊原因使您不想子类化
dict
?@Foon,它不会嵌套到任意深度,并且会引发异常(而不是返回默认值)如果链中早期的某个键不存在。@JohnY-有几个原因-我希望有一些方法可以在dict对象上执行此操作,而不必将它们强制转换为MyDict对象,我很好奇,如果不将dict子类化,这是否可行。否则,子类化工作得很好。我想不出一种方法使它递归。dict.get()
也是如此。这不是我关心的行为。@Jayhandren看到我的答案了。我已经调试了这些函数,现在它们已用于生产。答案在这里:谢谢!我想知道是否有一种Python-idomatic的方式来实现这一点;使用空的DITCS作为默认值为<代码> GET()/<代码>,使用匿名函数看起来都是很好的习惯用法。尽管这回答了OP的问题,但我认为JPP的答案更简洁。在某些情况下,提出一个
KeyError
比返回一个空的dict更自然。此外,jpp的答案更通用,因为它可以用于嵌套字典、嵌套列表以及两者的混合。请注意,这也适用于嵌套列表。如果任何索引超出范围,则使用
getitem
的变量将引发一个
indexer
。更好的是,建议的答案可用于同时包含嵌套列表和嵌套dict的dict,这在处理json数据时非常有用……我忘了提到元组和任何实现
\uuu getitem\uuu
方法的对象。。。
keys = ('options', 'style', 'body', 'name')

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