Python - 将扁平化的字典转换为嵌套字典

10

我有一个多维字典:

a = {'a' : 'b', 'c' : {'d' : 'e'}}

我已经编写了一个简单的函数来展开该字典:

def __flatten(self, dictionary, level = []):
    tmp_dict = {}
    for key, val in dictionary.items():
        if type(val) == dict:
            tmp_dict.update(self.__flatten(val, level + [key]))
        else:
            tmp_dict['.'.join(level + [key])] = val
    return tmp_dict

调用这个函数并使用字典 a,我得到的结果如下:
{'a' : 'b', 'c.d' : 'e'}

现在,在对此扁平化字典进行了几个指令之后,我需要从中构建新的多维字典。例如:

>> unflatten({'a' : 0, 'c.d' : 1))
{'a' : 0, 'c' : {'d' : 1}}

我唯一的问题是我没有一个unflatten函数 :)
有人能帮忙吗?我不知道该怎么做。 编辑: 另一个例子:
{'a' : 'b', 'c.d.e.f.g.h.i.j.k.l.m.n.o.p.r.s.t.u.w' : 'z'}

应该在展开之后进行操作:
{'a': 'b', 'c': {'d': {'e': {'f': {'g': {'h': {'i': {'j': {'k': {'l': {'m': {'n': {'o': {'p': {'r': {'s': {'t': {'u': {'w': 'z'}}}}}}}}}}}}}}}}}}}

还有一个:

{'a' : 'b', 'c.d' : 'z', 'c.e' : 1}

致:

{'a' : 'b', 'c' : {'d' : 'z', 'e' : 1}}

这大大增加了任务的难度,我知道这一点。这就是为什么我在处理此事时遇到问题并花费数小时找不到解决方案的原因。


真的我不明白为什么 'c': {'d': 'e'} 变成了 'c.d': 'e',如果你有 'c': {'f': 'g', 'd': 'e'} 会怎么样。 - neurino
然后 {'c' : {'f' : 'g', 'd' : 'e'}} 变成了 {'c.f' : 'g', 'c.d' : 'e'} :) - Galmi
5个回答

29
def unflatten(dictionary):
    resultDict = dict()
    for key, value in dictionary.items():
        parts = key.split(".")
        d = resultDict
        for part in parts[:-1]:
            if part not in d:
                d[part] = dict()
            d = d[part]
        d[parts[-1]] = value
    return resultDict

这个实现比我的更好,因为它可以处理输入 {'a': 'b', 'c.d.e': 1} 和任何更深层次的嵌套(更多点)。我不敢降低自己的回答评分,不确定会发生什么。;) - John Gaines Jr.
它完全按照我的需求工作。谢谢你,你让我省下了整整一晚上的思考 :) - Galmi
将Python 3.x中的iteritems替换为items - diogo

4
from collections import defaultdict
def unflatten(d):
    ret = defaultdict(dict)
    for k,v in d.items():
        k1,delim,k2 = k.partition('.')
        if delim:
            ret[k1].update({k2:v})
        else:
            ret[k1] = v
    return ret

使用递归是一个好的选择,但是不正确:例如 {'a.b': 0, 'a.c': 1} 将返回 {'a': {'c': 1}} :) 可以通过使用 ret[k1].update(...) 来解决。 - Lucas Moeskops
@Lucasmus,是的,我现在已将它改回我的原始答案。我瞬间认为它可以简化。 - John La Rooy

4

这里是一个利用Python 3.5+的特性,例如类型注释和解构赋值的示例。 在repl.it上尝试运行测试

from typing import Any, Dict


def unflatten(
    d: Dict[str, Any], 
    base: Dict[str, Any] = None,
) -> Dict[str, Any]:
    """Convert any keys containing dotted paths to nested dicts

    >>> unflatten({'a': 12, 'b': 13, 'c': 14})  # no expansion
    {'a': 12, 'b': 13, 'c': 14}

    >>> unflatten({'a.b.c': 12})  # dotted path expansion
    {'a': {'b': {'c': 12}}}

    >>> unflatten({'a.b.c': 12, 'a': {'b.d': 13}})  # merging
    {'a': {'b': {'c': 12, 'd': 13}}}

    >>> unflatten({'a.b': 12, 'a': {'b': 13}})  # insertion-order overwrites
    {'a': {'b': 13}}

    >>> unflatten({'a': {}})  # insertion-order overwrites
    {'a': {}}
    """
    if base is None:
        base = {}

    for key, value in d.items():
        root = base

        ###
        # If a dotted path is encountered, create nested dicts for all but
        # the last level, then change root to that last level, and key to
        # the final key in the path.
        #
        # This allows one final setitem at the bottom of the loop.
        #
        if '.' in key:
            *parts, key = key.split('.')

            for part in parts:
                root.setdefault(part, {})
                root = root[part]

        if isinstance(value, dict):
            value = unflatten(value, root.get(key, {}))

        root[key] = value

    return base

好的文档化代码加一分。 - Emerson Rocha

3

我一年前用Python 2和3写了一个我已经改编成以下形式的程序。它可以更轻松地检查一个给定的字典是否是一个更大的字典的子集,不管它是以平铺还是支架形式提供的。

额外的功能:如果存在连续的整数索引(如0、1、2、3、4等),这也会将它们转换回列表。

def unflatten_dictionary(field_dict):
    field_dict = dict(field_dict)
    new_field_dict = dict()
    field_keys = list(field_dict)
    field_keys.sort()

    for each_key in field_keys:
        field_value = field_dict[each_key]
        processed_key = str(each_key)
        current_key = None
        current_subkey = None
        for i in range(len(processed_key)):
            if processed_key[i] == "[":
                current_key = processed_key[:i]
                start_subscript_index = i + 1
                end_subscript_index = processed_key.index("]")
                current_subkey = int(processed_key[start_subscript_index : end_subscript_index])

                # reserve the remainder descendant keys to be processed later in a recursive call
                if len(processed_key[end_subscript_index:]) > 1:
                    current_subkey = "{}.{}".format(current_subkey, processed_key[end_subscript_index + 2:])
                break
            # next child key is a dictionary
            elif processed_key[i] == ".":
                split_work = processed_key.split(".", 1)
                if len(split_work) > 1:
                    current_key, current_subkey = split_work
                else:
                    current_key = split_work[0]
                break

        if current_subkey is not None:
            if current_key.isdigit():
                current_key = int(current_key)
            if current_key not in new_field_dict:
                new_field_dict[current_key] = dict()
            new_field_dict[current_key][current_subkey] = field_value
        else:
            new_field_dict[each_key] = field_value

    # Recursively unflatten each dictionary on each depth before returning back to the caller.
    all_digits = True
    highest_digit = -1
    for each_key, each_item in new_field_dict.items():
        if isinstance(each_item, dict):
            new_field_dict[each_key] = unflatten_dictionary(each_item)

        # validate the keys can safely converted to a sequential list.
        all_digits &= str(each_key).isdigit()
        if all_digits:
            next_digit = int(each_key)
            if next_digit > highest_digit:
                highest_digit = next_digit

    # If all digits and can be sequential order, convert to list.
    if all_digits and highest_digit == (len(new_field_dict) - 1):
        digit_keys = list(new_field_dict)
        digit_keys.sort()
        new_list = []

        for k in digit_keys:
            i = int(k)
            if len(new_list) <= i:
                # Pre-populate missing list elements if the array index keys are out of order
                # and the current element is ahead of the current length boundary.
                while len(new_list) <= i:
                    new_list.append(None)
            new_list[i] = new_field_dict[k]
        new_field_dict = new_list
    return new_field_dict

# Test
if __name__ == '__main__':
    input_dict = {'a[0]': 1,
                  'a[1]': 10,
                  'a[2]': 5,
                  'b': 10,
                  'c.test.0': "hi",
                  'c.test.1': "bye",
                  "c.head.shoulders": "richard",
                  "c.head.knees": 'toes',
                  "z.trick.or[0]": "treat",
                  "z.trick.or[1]": "halloween",
                  "z.trick.and.then[0]": "he",
                  "z.trick.and.then[1]": "it",
                  "some[0].nested.field[0]": 42,
                  "some[0].nested.field[1]": 43,
                  "some[2].nested.field[0]": 44,
                  "mixed": {
                      "statement": "test",
                      "break[0]": True,
                      "break[1]": False,
                  }}
    expected_dict = {'a': [1, 10, 5],
                     'b': 10,
                     'c': {
                         'test': ['hi', 'bye'],
                         'head': {
                             'shoulders': 'richard',
                             'knees' : 'toes'
                         }
                     },
                     'z': {
                         'trick': {
                             'or': ["treat", "halloween"],
                             'and': {
                                 'then': ["he", "it"]
                             }
                         }
                     },
                     'some': {
                         0: {
                             'nested': {
                                 'field': [42, 43]
                             }
                         },
                         2: {
                             'nested': {
                                 'field': [44]
                             }
                         }
                     },
                     "mixed": {
                         "statement": "test",
                         "break": [True, False]
                     }}
    # test
    print("Input:")
    print(input_dict)
    print("====================================")
    print("Output:")
    actual_dict = unflatten_dictionary(input_dict)
    print(actual_dict)
    print("====================================")
    print(f"Test passed? {expected_dict==actual_dict}")
    

1
支持通过索引列表项的唯一解决方案点赞,即keyword[0] = "sometext" --> keyword: ["sometext"]。 - Nicolai Prebensen
@NicolaiPrebensen 谢谢。这可能是我最有趣的实现之一。 - djtubig-malicex

1
作为一个草稿(变量名选择可能需要改进,也许还有一些鲁棒性问题,但针对给定的示例它可以工作):
def unflatten(d):
    result = {}
    for k,v in d.iteritems():
        if '.' in k:
            k1, k2 = k.split('.', 1)
            v = {k2: v}
            k = k1
        result[k] = v
    return result

1
糟糕!即使我足够男人承认自己的回答不如另一个(在这种情况下是Messa),我也不能对自己的回答进行踩。 - John Gaines Jr.

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