通过键列表访问嵌套字典项?

216 投票
22 回答
187248 浏览
提问于 2025-04-17 14:43

我有一个复杂的字典结构,我想通过一系列的键来访问正确的项目。

dataDict = {
    "a":{
        "r": 1,
        "s": 2,
        "t": 3
        },
    "b":{
        "u": 1,
        "v": {
            "x": 1,
            "y": 2,
            "z": 3
            },
        "w": 3
        }
    }    

maplist = ["a", "r"]

或者

maplist = ["b", "v", "y"]

我写了以下代码,它可以正常工作,但我相信如果有人有更好的想法,肯定有更好更高效的方法来做到这一点。

# Get a given data from a dictionary with position provided as a list
def getFromDict(dataDict, mapList):    
    for k in mapList:
        dataDict = dataDict[k]
    return dataDict

# Set a given data in a dictionary with position provided as a list
def setInDict(dataDict, mapList, value): 
    for k in mapList[:-1]:
        dataDict = dataDict[k]
    dataDict[mapList[-1]] = value

22 个回答

16

使用reduce方法很聪明,但如果父键在嵌套字典中不存在,楼主的set方法可能会有问题。因为这是我在谷歌搜索这个主题时看到的第一个Stack Overflow帖子,所以我想稍微改进一下。

在(给定索引列表和数值设置嵌套Python字典中的值)中的set方法似乎对缺失的父键更稳健。为了方便起见,我把它复制过来:

def nested_set(dic, keys, value):
    for key in keys[:-1]:
        dic = dic.setdefault(key, {})
    dic[keys[-1]] = value

另外,拥有一个可以遍历键树并获取所有绝对键路径的方法也很方便,我为此创建了:

def keysInDict(dataDict, parent=[]):
    if not isinstance(dataDict, dict):
        return [tuple(parent)]
    else:
        return reduce(list.__add__, 
            [keysInDict(v,parent+[k]) for k,v in dataDict.items()], [])

它的一个用途是将嵌套树转换为pandas的DataFrame,使用以下代码(假设嵌套字典中的所有叶子节点深度相同)。

def dict_to_df(dataDict):
    ret = []
    for k in keysInDict(dataDict):
        v = np.array( getFromDict(dataDict, k), )
        v = pd.DataFrame(v)
        v.columns = pd.MultiIndex.from_product(list(k) + [v.columns])
        ret.append(v)
    return reduce(pd.DataFrame.join, ret)
85

使用 for 循环看起来更符合 Python 的风格。你可以看看这段来自 Python 3.0 新特性 的引用。

移除了 reduce() 函数。如果你真的需要它,可以使用 functools.reduce();不过,99% 的情况下,直接用 for 循环会更容易理解。

def nested_get(dic, keys):    
    for key in keys:
        dic = dic[key]
    return dic

def nested_set(dic, keys, value):
    for key in keys[:-1]:
        dic = dic.setdefault(key, {})
    dic[keys[-1]] = value

def nested_del(dic, keys):
    for key in keys[:-1]:
        dic = dic[key]
    del dic[keys[-1]]

需要注意的是,接受的解决方案并不会设置不存在的嵌套键(会抛出 KeyError 错误)。而使用上面的方法会创建不存在的节点。

这段代码在 Python 2 和 3 中都能运行。

336

使用 reduce() 来遍历字典:

from functools import reduce  # forward compatibility for Python 3
import operator

def getFromDict(dataDict, mapList):
    return reduce(operator.getitem, mapList, dataDict)

然后可以重复使用 getFromDict 来找到存储值的位置,以便使用 setInDict()

def setInDict(dataDict, mapList, value):
    getFromDict(dataDict, mapList[:-1])[mapList[-1]] = value

mapList 中,除了最后一个元素,其他的元素都是用来找到“父”字典,以便把值添加进去,然后用最后一个元素来把值设置到正确的键上。

示例:

>>> getFromDict(dataDict, ["a", "r"])
1
>>> getFromDict(dataDict, ["b", "v", "y"])
2
>>> setInDict(dataDict, ["b", "v", "w"], 4)
>>> import pprint
>>> pprint.pprint(dataDict)
{'a': {'r': 1, 's': 2, 't': 3},
 'b': {'u': 1, 'v': {'w': 4, 'x': 1, 'y': 2, 'z': 3}, 'w': 3}}

注意,Python 的 PEP8 风格指南 建议函数使用蛇形命名法。上面的代码同样适用于列表或者字典和列表的混合,所以函数的名字其实应该是 get_by_path()set_by_path()

from functools import reduce  # forward compatibility for Python 3
import operator

def get_by_path(root, items):
    """Access a nested object in root by item sequence."""
    return reduce(operator.getitem, items, root)

def set_by_path(root, items, value):
    """Set a value in a nested object in root by item sequence."""
    get_by_path(root, items[:-1])[items[-1]] = value

为了完整性,这里还有一个删除键的函数:

def del_by_path(root, items):
    """Delete a key-value in a nested object in root by item sequence."""
    del get_by_path(root, items[:-1])[items[-1]]

撰写回答