2022年 11月 5日

Python之字典以层级形式输出

文章目录

    • 问题
    • 分析
    • 进一步
    • 再进一步

问题

现有字典:

a = dict(a0=1, b0=2, c0=3, d0=dict(a1=[1, 2, 3], b1=dict(a2=dict(a3=10, b3=[1, 2, 3])), c1=dict(a2=1)))
  • 1

想将其按照不同的层级进行输出,即如下形式:

{
    a0: 1
    b0: 2
    c0: 3
    d0: {
        a1: [1, 2, 3]
        b1: {
            a2: {
                a3: 10
                b3: [1, 2, 3]
            }
        }
        c1: {
            a2: 1
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

如何构造函数mapping2str

分析

首先直观上的一个理解是,这里的多层嵌套,由于深度是未知的,所以是要使用递归的方式进行处理的。也就是要反复调用mapping2str

大体结构可以得到如下形式:

def mapping2str(mapping: abc.Mapping, **kwargs) -> str:
    ...
    for key, value in mapping.items():
        # 仅在value为字典时才会递归调用
        if isinstance(value, abc.Mapping):
            # 进入下一字典层级中
            value = mapping_to_str(value)
        else:
            # 对于非字典项,直接转化为字符串
            value = str(value)
        out += value
    ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这里实际上很直观,基本上可以处理嵌套字典的不同层级。接下来就是缩进和花括号包裹的问题了。

我们观察最终输出的形式:

{
    a0: 1
    b0: 2
    c0: 3
    d0: {
        a1: [1, 2, 3]
        b1: {
            a2: {
                a3: 10
                b3: [1, 2, 3]
            }
        }
        c1: {
            a2: 1
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

对于这里的字典,关于缩进和花括号的形式有如下特征:

  • 左括号位于f"{key}:"之后,
  • 右括号缩进与f"{key}:"一致
  • 各个同级的子项缩进一致,且比父级更缩进一级
  • 仅当子项为字典时会绘制花括号

将这些内容补充到前面的代码中:

def mapping2str(mapping: abc.Mapping, prefix: str = "    ", lvl: int = 0) -> str:
    # prefix = "    "  为基础缩进符号
    # lvl = 0  为整数,表示缩进的层级,以最外层花括号的缩进作为0级,之后逐层增1,则各层的缩进符号可以写为 prefix * lvl
    sub_lvl = lvl + 1
    sub_prefix = prefix * sub_lvl

    sub_items = []
    for key, value in mapping.items():
        sub_item = sub_prefix + key + ": "
        # 仅在value为字典时才会递归调用
        if isinstance(value, abc.Mapping):  # 可以看做是在模拟d0中的b1
            # 进入下一字典层级中
            sub_item += "{\n"
            sub_item += mapping2str(value, prefix, sub_lvl)  # 递归调用的部分实际上可以看做是在模拟b1中的a2
            sub_item += "\n" + sub_prefix + "}"
        else:
            # 对于非字典项,直接转化为字符串
            sub_item += str(value)  # 模拟d0中的a1
        sub_items.append(sub_item)
    # 使用'\n'连接a1、b1、c1
    sub_items = "\n".join(sub_items)
    return sub_items
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

此时输出为:

    a0: 1
    b0: 2
    c0: 3
    d0: {
        a1: [1, 2, 3]
        b1: {
            a2: {
                a3: 10
                b3: [1, 2, 3]
            }
        }
        c1: {
            a2: 1
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

可见缺失了头尾的花括号,可以在最后根据lvl来补上他们:

def mapping2str(mapping: abc.Mapping, prefix: str = "    ", lvl: int = 0) -> str:
    # prefix = "    "  为基础缩进符号
    # lvl = 0  为整数,表示缩进的层级,以最外层花括号的缩进作为0级,之后逐层增1,则各层的缩进符号可以写为 prefix * lvl
    sub_lvl = lvl + 1
    sub_prefix = prefix * sub_lvl

    sub_items = []
    for key, value in mapping.items():
        sub_item = sub_prefix + key + ": "
        # 仅在value为字典时才会递归调用
        if isinstance(value, abc.Mapping):  # 可以看做是在模拟d0中的b1
            # 进入下一字典层级中
            sub_item += "{\n"
            sub_item += mapping2str(value, prefix, sub_lvl)  # 递归调用的部分实际上可以看做是在模拟b1中的a2
            sub_item += "\n" + sub_prefix + "}"
        else:
            # 对于非字典项,直接转化为字符串
            sub_item += str(value)  # 模拟d0中的a1
        sub_items.append(sub_item)
    # 使用'\n'连接a1、b1、c1
    sub_items = "\n".join(sub_items)
    if lvl == 0:
        # 加上头尾的花括号
        sub_items = '{\n' + sub_items + '\n}'
    return sub_items
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

进一步

虽然上面的代码已经实现了想要的目的,但是总觉的最后的那个花括号有些扎眼。有没有办法把它去掉呢?
直接的想法就是在前面构造每一项的过程中,自动的包含头尾的花括号。
于是,我们尝试重构代码,得到一个新的版本:

def mapping2str_v1(mapping: abc.Mapping, prefix: str = "    ", lvl: int = 0) -> str:
    # prefix = "    "  为基础缩进符号
    # lvl = 0  为整数,表示缩进的层级,以最外层花括号的缩进作为0级,之后逐层增1,则各层的缩进符号可以写为 prefix * lvl
    sub_lvl = lvl + 1
    cur_prefix = prefix * lvl
    sub_prefix = prefix * sub_lvl

    sub_items = ["{"]  # 这一部分的缩进由上一层的sub_item的起始字符`sub_prefix + key + ": `确定
    for key, value in mapping.items():
        sub_item = sub_prefix + key + ": "
        # 仅在value为字典时才会递归调用
        if isinstance(value, abc.Mapping):  # 可以看做是在模拟d0中的b1
            # 进入下一字典层级中
            sub_item += mapping2str_v1(value, prefix, sub_lvl)  # 递归调用的部分实际上可以看做是在模拟b1中的a2
        else:
            # 对于非字典项,直接转化为字符串
            sub_item += str(value)  # 模拟d0中的a1
        sub_items.append(sub_item)
    sub_items.append(cur_prefix + "}")
    # 使用'\n'连接a1、b1、c1
    sub_items = "\n".join(sub_items)
    return sub_items
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

输出为:

{
    a0: 1
    b0: 2
    c0: 3
    d0: {
        a1: [1, 2, 3]
        b1: {
            a2: {
                a3: 10
                b3: [1, 2, 3]
            }
        }
        c1: {
            a2: 1
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

再进一步

如果我们要求,额外传入一个参数限制展开字典的最大层级,即,如果max_lvl=0则表示整个输出直接写成一行,如果max_lvl=1,则表示各个*0的孩子都写成一行,所谓展开1级。后面的层级以此类推。

可以写成如下代码:

def mapping2str_v2(mapping: abc.Mapping, *, prefix: str = "    ", lvl: int = 0, max_lvl: int = 3) -> str:
    sub_lvl = lvl + 1
    cur_prefix = prefix * lvl
    sub_prefix = prefix * sub_lvl

    if lvl == max_lvl:  # 用来处理最大层级下的各项内容,直接按照字符串一行输出
        sub_items = str(mapping)
    else:
        sub_items = ["{"]
        for k, v in mapping.items():
            sub_item = sub_prefix + k + ": "
            if isinstance(v, abc.Mapping):
                sub_item += mapping2str_v2(v, prefix=prefix, lvl=sub_lvl, max_lvl=max_lvl)
            else:
                sub_item += str(v)  # 更多是用于处理各个层级非字典项的输出
            sub_items.append(sub_item)
        sub_items.append(cur_prefix + "}")
        sub_items = "\n".join(sub_items)
    return sub_items
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

各个输出为:

print("max_lvl=0 -->\n", mapping2str_v2(a, max_lvl=0))
print("max_lvl=1 -->\n", mapping2str_v2(a, max_lvl=1))
print("max_lvl=2 -->\n", mapping2str_v2(a, max_lvl=2))
print("max_lvl=3 -->\n", mapping2str_v2(a, max_lvl=3))
print("max_lvl=4 -->\n", mapping2str_v2(a, max_lvl=4))
print("max_lvl=5 -->\n", mapping2str_v2(a, max_lvl=5))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
max_lvl=0 -->
 {'a0': 1, 'b0': 2, 'c0': 3, 'd0': {'a1': [1, 2, 3], 'b1': {'a2': {'a3': 10, 'b3': [1, 2, 3]}}, 'c1': {'a2': 1}}}
max_lvl=1 -->
 {
    a0: 1
    b0: 2
    c0: 3
    d0: {'a1': [1, 2, 3], 'b1': {'a2': {'a3': 10, 'b3': [1, 2, 3]}}, 'c1': {'a2': 1}}
}
max_lvl=2 -->
 {
    a0: 1
    b0: 2
    c0: 3
    d0: {
        a1: [1, 2, 3]
        b1: {'a2': {'a3': 10, 'b3': [1, 2, 3]}}
        c1: {'a2': 1}
    }
}
max_lvl=3 -->
 {
    a0: 1
    b0: 2
    c0: 3
    d0: {
        a1: [1, 2, 3]
        b1: {
            a2: {'a3': 10, 'b3': [1, 2, 3]}
        }
        c1: {
            a2: 1
        }
    }
}
max_lvl=4 -->
 {
    a0: 1
    b0: 2
    c0: 3
    d0: {
        a1: [1, 2, 3]
        b1: {
            a2: {
                a3: 10
                b3: [1, 2, 3]
            }
        }
        c1: {
            a2: 1
        }
    }
}
max_lvl=5 -->
 {
    a0: 1
    b0: 2
    c0: 3
    d0: {
        a1: [1, 2, 3]
        b1: {
            a2: {
                a3: 10
                b3: [1, 2, 3]
            }
        }
        c1: {
            a2: 1
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71