从扁平字典创建嵌套字典

Posted

技术标签:

【中文标题】从扁平字典创建嵌套字典【英文标题】:Creating a nested dictionary from a flattened dictionary 【发布时间】:2018-11-09 10:52:41 【问题描述】:

我有一个扁平化的字典,我想把它做成一个嵌套的字典,格式为

flat = 'X_a_one': 10,
        'X_a_two': 20, 
        'X_b_one': 10,
        'X_b_two': 20, 
        'Y_a_one': 10,
        'Y_a_two': 20,
        'Y_b_one': 10,
        'Y_b_two': 20

我想转成表格

nested = 'X': 'a': 'one': 10,
                      'two': 20, 
                'b': 'one': 10,
                      'two': 20, 
          'Y': 'a': 'one': 10,
                      'two': 20,
                'b': 'one': 10,
                      'two': 20

平面字典的结构是这样的,不应该有任何歧义问题。我希望它适用于任意深度的词典,但性能并不是真正的问题。我见过很多扁平化嵌套字典的方法,但基本上没有嵌套扁平化字典的方法。存储在字典中的值要么是标量,要么是字符串,绝不是可迭代的。

到目前为止,我已经得到了可以接受输入的东西

test_dict = 'X_a_one': '10',
             'X_b_one': '10',
             'X_c_one': '10'

输出

test_out = 'X': 'a_one': '10', 
                  'b_one': '10', 
                  'c_one': '10'

使用代码

def nest_once(inp_dict):
    out = 
    if isinstance(inp_dict, dict):
        for key, val in inp_dict.items():
            if '_' in key:
                head, tail = key.split('_', 1)

                if head not in out.keys():
                    out[head] = tail: val
                else:
                    out[head].update(tail: val)
            else:
                out[key] = val
    return out

test_out = nest_once(test_dict)

但我无法弄清楚如何将它变成递归创建字典所有级别的东西。

任何帮助将不胜感激!

(至于我为什么要这样做:我有一个结构相当于嵌套字典的文件,我想将此文件的内容存储在 NetCDF 文件的属性字典中并稍后检索它。但是只有 NetCDF允许您将平面字典作为属性,因此我想将之前存储在 NetCDF 文件中的字典展开。)

【问题讨论】:

写得很好的问题。 【参考方案1】:
output = 

for k, v in source.items():
    # always start at the root.
    current = output

    # This is the part you're struggling with.
    pieces = k.split('_')

    # iterate from the beginning until the second to last place
    for piece in pieces[:-1]:
       if not piece in current:
          # if a dict doesn't exist at an index, then create one
          current[piece] = 

       # as you walk into the structure, update your current location
       current = current[piece]

    # The reason you're using the second to last is because the last place
    # represents the place you're actually storing the item
    current[pieces[-1]] = v

【讨论】:

在我看来,更具可读性的是在线解包:*initial_keys, final_key = k.split('_')。但是很好的答案! 这真是太好了,我喜欢它不使用递归的方式。【参考方案2】:

这是使用collections.defaultdict 的一种方式,大量借鉴this previous answer。有3个步骤:

    创建嵌套的defaultdict defaultdict 对象。 迭代flat 输入字典中的项目。 根据_拆分键得到的结构构建defaultdict结果,使用getFromDict迭代结果字典。

这是一个完整的例子:

from collections import defaultdict
from functools import reduce
from operator import getitem

def getFromDict(dataDict, mapList):
    """Iterate nested dictionary"""
    return reduce(getitem, mapList, dataDict)

# instantiate nested defaultdict of defaultdicts
tree = lambda: defaultdict(tree)
d = tree()

# iterate input dictionary
for k, v in flat.items():
    *keys, final_key = k.split('_')
    getFromDict(d, keys)[final_key] = v

'X': 'a': 'one': 10, 'two': 20, 'b': 'one': 10, 'two': 20,
 'Y': 'a': 'one': 10, 'two': 20, 'b': 'one': 10, 'two': 20

作为最后一步,您可以将defaultdict 转换为普通的dict,但通常这一步不是必需的。

def default_to_regular_dict(d):
    """Convert nested defaultdict to regular dict of dicts."""
    if isinstance(d, defaultdict):
        d = k: default_to_regular_dict(v) for k, v in d.items()
    return d

# convert back to regular dict
res = default_to_regular_dict(d)

【讨论】:

*keys, final_key = ... – 这是什么魔法? :O 顺便说一下,+1。 @DavidFoerster,这会将k.split('_') 生成的列表解包为一个列表和一个字符串,其中字符串是最终拆分。它消除了以后对位置索引的需要。 我可以暗示这么多,但我完全不知道这种语言功能。【参考方案3】:

其他答案更简洁,但既然你提到了递归,我们确实有其他选择。

def nest(d):
    _ = 
    for k in d:
        i = k.find('_')
        if i == -1:
            _[k] = d[k]
            continue
        s, t = k[:i], k[i+1:]
        if s in _:
            _[s][t] = d[k]
        else:
            _[s] = t:d[k]
    return k:(nest(_[k]) if type(_[k])==type(d) else _[k]) for k in _

【讨论】:

【参考方案4】:

这是我的看法:

def nest_dict(flat):
    result = 
    for k, v in flat.items():
        _nest_dict_rec(k, v, result)
    return result

def _nest_dict_rec(k, v, out):
    k, *rest = k.split('_', 1)
    if rest:
        _nest_dict_rec(rest[0], v, out.setdefault(k, ))
    else:
        out[k] = v

flat = 'X_a_one': 10,
        'X_a_two': 20, 
        'X_b_one': 10,
        'X_b_two': 20, 
        'Y_a_one': 10,
        'Y_a_two': 20,
        'Y_b_one': 10,
        'Y_b_two': 20
nested = 'X': 'a': 'one': 10,
                      'two': 20, 
                'b': 'one': 10,
                      'two': 20, 
          'Y': 'a': 'one': 10,
                      'two': 20,
                'b': 'one': 10,
                      'two': 20
print(nest_dict(flat) == nested)
# True

【讨论】:

这是最接近我想象的解决方案,谢谢! 我喜欢这个聪明的递归算法!【参考方案5】:

你可以使用itertools.groupby:

import itertools, json
flat = 'Y_a_two': 20, 'Y_a_one': 10, 'X_b_two': 20, 'X_b_one': 10, 'X_a_one': 10, 'X_a_two': 20, 'Y_b_two': 20, 'Y_b_one': 10
_flat = [[*a.split('_'), b] for a, b in flat.items()]
def create_dict(d): 
  _d = a:list(b) for a, b in itertools.groupby(sorted(d, key=lambda x:x[0]), key=lambda x:x[0])
  return a:create_dict([i[1:] for i in b]) if len(b) > 1 else b[0][-1] for a, b in _d.items()

print(json.dumps(create_dict(_flat), indent=3))

输出:


 "Y": 
    "b": 
      "two": 20,
      "one": 10
    ,
    "a": 
      "two": 20,
      "one": 10
    
 ,
  "X": 
     "b": 
     "two": 20,
     "one": 10
   ,
    "a": 
     "two": 20,
     "one": 10
   
 

【讨论】:

我喜欢使用 groupby!,但恐怕这比其他解决方案可读性差。【参考方案6】:

另一个没有导入的非递归解决方案。拆分插入平面字典的每个键值对和映射平面字典的键值对之间的逻辑。

def insert(dct, lst):
    """
    dct: a dict to be modified inplace.
    lst: list of elements representing a hierarchy of keys
    followed by a value.

    dct = 
    lst = [1, 2, 3]

    resulting value of dct: 1: 2: 3
    """
    for x in lst[:-2]:
        dct[x] = dct = dct.get(x, dict())

    dct.update(lst[-2]: lst[-1])


def unflat(dct):
    # empty dict to store the result
    result = dict()

    # create an iterator of lists representing hierarchical indices followed by the value
    lsts = ([*k.split("_"), v] for k, v in dct.items())

    # insert each list into the result
    for lst in lsts:
        insert(result, lst)

    return result


result = unflat(flat)
# 'X': 'a': 'one': 10, 'two': 20, 'b': 'one': 10, 'two': 20,
# 'Y': 'a': 'one': 10, 'two': 20, 'b': 'one': 10, 'two': 20

【讨论】:

【参考方案7】:

这是一个可读性强的递归结果:

def unflatten_dict(a, result = None, sep = '_'):

    if result is None:
        result = dict()

    for k, v in a.items():
        k, *rest = k.split(sep, 1)
        if rest:
            unflatten_dict(rest[0]: v, result.setdefault(k, ), sep = sep)
        else:
            result[k] = v

    return result


flat = 'X_a_one': 10,
        'X_a_two': 20,
        'X_b_one': 10,
        'X_b_two': 20,
        'Y_a_one': 10,
        'Y_a_two': 20,
        'Y_b_one': 10,
        'Y_b_two': 20

print(unflatten_dict(flat))
# 'X': 'a': 'one': 10, 'two': 20, 'b': 'one': 10, 'two': 20, 
#  'Y': 'a': 'one': 10, 'two': 20, 'b': 'one': 10, 'two': 20

这是基于上述几个答案,不使用导入,仅在 python 3 中测试。

【讨论】:

以上是关于从扁平字典创建嵌套字典的主要内容,如果未能解决你的问题,请参考以下文章

使用 pandas json_normalize 扁平化包含多个嵌套列表的字典列表

Python [练习题] :字典扁平化

Python [习题] 字典扁平化

如何在dynamoDB,嵌套字典或多个项目中构建数据?

字典的扁平化引发 AttributeError

我怎样才能扁平化一个字典列表?