从扁平化的字典创建嵌套字典

52

我有一个扁平的字典,我想把它变成嵌套的形式

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文件中的字典。)


9
写得很好的问题。 - timgeb
8个回答

28

这是我的观点:

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

这是最接近我所想象的解决方案,谢谢! - ThomasNicholas
我喜欢这个巧妙的递归算法! - Bruno Vermeulen

24
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

3
我认为更易读的方式是将*initial_keys, final_key = k.split('_')分解开来。但您的回答很棒! - jpp
这真的很好,我喜欢它不使用递归的方式。 - ThomasNicholas

15

这里有一种使用 collections.defaultdict 的方法,从 这个先前的答案 借鉴。 其中有 3 个步骤:

  1. 创建一个嵌套的 defaultdict 对象的字典。
  2. 迭代 flat 输入字典中的项目。
  3. 根据通过 _ 分割键得到的结构,使用 getFromDict 迭代结果字典,构建 defaultdict 的结果。

这是一个完整的示例:

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。 - David Foerster
2
@DavidFoerster,这将k.split('_')生成的列表解包成一个列表和一个字符串,其中字符串是最后一次拆分。它消除了后续需要进行位置索引的需要。 - jpp
1
我可能能够推测到很多,但我完全不知道这种语言特性。 - David Foerster
1
我只想说,仅仅通过阅读这篇文章,我就成为了一个更好的程序员。这太神奇了。点赞! - Daniel Soutar

4
其他答案更加简洁,但是既然你提到了递归,我们还有其他选择。
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 _}

为什么不为生成的字典使用一个 普通 的变量名呢? - funnydman
1
减少在搜索我的代码复制时出现误报;)尽管已经过去很多年了,但我不确定为什么会选择那个。我倾向于使用下划线前缀的短名称来表示特别临时的数据,这可能是所有想法都消失的原因。 - Hans Musgrave

4
您可以使用 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
   }
 }
}

4
我喜欢使用groupby!但是我担心这种方法比其他解决方案不易读懂。 - jabellcu

4

这是一个没有导入任何库的非递归解决方案。将逻辑分为插入每个键值对和映射平面字典的键值对之间。

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}}}

3

以下是一个相对易读的递归结果:

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中测试过。"Original Answer"翻译成"最初的回答"。

0

安装ndicts

pip install ndicts

然后在你的脚本中

from ndicts.ndicts import NestedDict

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}

nd = NestedDict()
for key, value in flat.items():
    n_key = tuple(key.split("_"))
    nd[n_key] = value

如果您需要将结果作为字典返回:
>>> nd.to_dict()
{'X': {'a': {'one': 10, 'two': 20}, 
       'b': {'one': 10, 'two': 20}}, 
 'Y': {'a': {'one': 10, 'two': 20},
       'b': {'one': 10, 'two': 20}}}

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