关于python:拼合嵌套词典,压缩键

Flatten nested dictionaries, compressing keys

假设你有一本像这样的字典:

1
2
3
4
5
{'a': 1,
 'c': {'a': 2,
       'b': {'x': 5,
             'y' : 10}},
 'd': [1, 2, 3]}

你将如何把它展平成:

1
2
3
4
5
{'a': 1,
 'c_a': 2,
 'c_b_x': 5,
 'c_b_y': 10,
 'd': [1, 2, 3]}


基本上,与扁平嵌套列表的方法相同,您只需要做额外的工作,按键/值迭代dict,为新字典创建新键,并在最后一步创建字典。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import collections

def flatten(d, parent_key='', sep='_'):
    items = []
    for k, v in d.items():
        new_key = parent_key + sep + k if parent_key else k
        if isinstance(v, collections.MutableMapping):
            items.extend(flatten(v, new_key, sep=sep).items())
        else:
            items.append((new_key, v))
    return dict(items)

>>> flatten({'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]})
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}


原始海报需要考虑两个重要因素:

  • 有没有击键空间的问题?例如,{'a_b':{'c':1}, 'a':{'b_c':2}}会导致{'a_b_c':???}。下面的解决方案通过返回一个不可数对来避免这个问题。
  • 如果性能是一个问题,那么key reducer函数(我这里称之为"join")是否需要访问整个密钥路径,或者它只需要在树中的每个节点上执行o(1)操作?如果你想说joinedKey = '_'.join(*keys),那会花费你运行时间。但是,如果你愿意说"江户记1(3)",那你就有时间了。下面的解决方案允许您同时执行这两项操作(因为您只需连接所有键,然后对它们进行后处理)。
  • (性能不太可能是一个问题,但我将详细阐述第二点,以防其他人关心:在实现这一点时,有许多危险的选择。如果您递归地执行此操作并生成和重新生成,或者任何与节点接触多次的等效操作(这很容易意外地执行),那么您可能正在执行O(n^2)工作,而不是O(n)。这是因为你可能正在计算一个键a,然后是a_1,然后是a_1_i,然后是a,然后是a_1,然后是a_1_ii,但实际上你不需要再计算a_1。即使您没有重新计算它,重新生成它(一种"逐级"的方法)也同样糟糕。一个很好的例子是考虑{1:{1:{1:{1:...(N times)...{1:SOME_LARGE_DICTIONARY_OF_SIZE_N}...}}}}的性能)

    下面是我写的一个函数flattenDict(d, join=..., lift=...),它可以适应多种用途,并且可以做你想要做的事情。遗憾的是,在不招致上述性能损失的情况下,很难制作出这个函数的懒惰版本(许多像chain.from-iterable这样的Python内置实际上没有效率,这是我在解决这个问题之前,在对三个不同版本的代码进行了广泛的测试之后才意识到的)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    from collections import Mapping
    from itertools import chain
    from operator import add

    _FLAG_FIRST = object()

    def flattenDict(d, join=add, lift=lambda x:x):
        results = []
        def visit(subdict, results, partialKey):
            for k,v in subdict.items():
                newKey = lift(k) if partialKey==_FLAG_FIRST else join(partialKey,lift(k))
                if isinstance(v,Mapping):
                    visit(v, results, newKey)
                else:
                    results.append((newKey,v))
        visit(d, results, _FLAG_FIRST)
        return results

    为了更好地理解正在发生的事情,下面是不熟悉EDOCX1(左边)的人的图表,也就是所谓的"左折叠"。有时它是用初始值代替k0(不是列表的一部分,传递到函数中)绘制的。这里,J是我们的join功能。我们用lift(k)对每个kn进行预处理。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
                   [k0,k1,...,kN].foldleft(J)
                               /    \
                             ...    kN
                             /
           J(k0,J(k1,J(k2,k3)))
                           /  \
                          /    \
               J(J(k0,k1),k2)   k3
                        /   \
                       /     \
                 J(k0,k1)    k2
                     /  \
                    /    \
                   k0     k1

    这实际上与functools.reduce相同,但是我们的函数在树的所有关键路径上都这样做。

    1
    2
    >>> reduce(lambda a,b:(a,b), range(5))
    ((((0, 1), 2), 3), 4)

    演示(我把它放在docstring中):

    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
    >>> testData = {
            'a':1,
            'b':2,
            'c':{
                'aa':11,
                'bb':22,
                'cc':{
                    'aaa':111
                }
            }
        }
    from pprint import pprint as pp

    >>> pp(dict( flattenDict(testData, lift=lambda x:(x,)) ))
    {('a',): 1,
     ('b',): 2,
     ('c', 'aa'): 11,
     ('c', 'bb'): 22,
     ('c', 'cc', 'aaa'): 111}

    >>> pp(dict( flattenDict(testData, join=lambda a,b:a+'_'+b) ))
    {'a': 1, 'b': 2, 'c_aa': 11, 'c_bb': 22, 'c_cc_aaa': 111}    

    >>> pp(dict( (v,k) for k,v in flattenDict(testData, lift=hash, join=lambda a,b:hash((a,b))) ))
    {1: 12416037344,
     2: 12544037731,
     11: 5470935132935744593,
     22: 4885734186131977315,
     111: 3461911260025554326}

    性能:

    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
    from functools import reduce
    def makeEvilDict(n):
        return reduce(lambda acc,x:{x:acc}, [{i:0 for i in range(n)}]+range(n))

    import timeit
    def time(runnable):
        t0 = timeit.default_timer()
        _ = runnable()
        t1 = timeit.default_timer()
        print('took {:.2f} seconds'.format(t1-t0))

    >>> pp(makeEvilDict(8))
    {7: {6: {5: {4: {3: {2: {1: {0: {0: 0,
                                     1: 0,
                                     2: 0,
                                     3: 0,
                                     4: 0,
                                     5: 0,
                                     6: 0,
                                     7: 0}}}}}}}}}

    import sys
    sys.setrecursionlimit(1000000)

    forget = lambda a,b:''

    >>> time(lambda: dict(flattenDict(makeEvilDict(10000), join=forget)) )
    took 0.10 seconds
    >>> time(lambda: dict(flattenDict(makeEvilDict(100000), join=forget)) )
    [1]    12569 segmentation fault  python

    …叹息,别以为那是我的错…

    [由于适度问题,不重要的历史注释]

    关于所谓的python中的flatten a dictionary of dictionary(2 levels deep)of list的副本:

    这个问题的解决方案可以通过执行sorted( sum(flatten(...),[]) )来实现。相反的情况是不可能的:虽然通过映射一个高阶累加器可以从所谓的副本中恢复flatten(...)的值,但无法恢复密钥。(编辑:而且事实证明,所谓的重复所有者的问题完全不同,因为它只处理2级深度的字典,尽管该页上的一个答案给出了一般的解决方案。)


    或者,如果您已经在使用熊猫,您可以使用json_normalize()这样做:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import pandas as pd

    d = {'a': 1,
         'c': {'a': 2, 'b': {'x': 5, 'y' : 10}},
         'd': [1, 2, 3]}

    df = pd.io.json.json_normalize(d, sep='_')

    print(df.to_dict(orient='records')[0])

    输出:

    1
    {'a': 1, 'c_a': 2, 'c_b_x': 5, 'c_b_y': 10, 'd': [1, 2, 3]}


    这里是一种"功能性的"、"一行程序"实现。它是递归的,基于条件表达式和听写理解。

    1
    2
    3
    4
    5
    def flatten_dict(dd, separator='_', prefix=''):
        return { prefix + separator + k if prefix else k : v
                 for kk, vv in dd.items()
                 for k, v in flatten_dict(vv, separator, kk).items()
                 } if isinstance(dd, dict) else { prefix : dd }

    测试:

    1
    2
    3
    4
    5
    6
    7
    8
    In [2]: flatten_dict({'abc':123, 'hgf':{'gh':432, 'yu':433}, 'gfd':902, 'xzxzxz':{"432":{'0b0b0b':231},"43234":1321}}, '.')
    Out[2]:
    {'abc': 123,
     'gfd': 902,
     'hgf.gh': 432,
     'hgf.yu': 433,
     'xzxzxz.432.0b0b0b': 231,
     'xzxzxz.43234': 1321}


    代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    test = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]}

    def parse_dict(init, lkey=''):
        ret = {}
        for rkey,val in init.items():
            key = lkey+rkey
            if isinstance(val, dict):
                ret.update(parse_dict(val, key+'_'))
            else:
                ret[key] = val
        return ret

    print(parse_dict(test,''))

    结果:

    1
    2
    $ python test.py
    {'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}

    我正在使用python3.2,更新您的python版本。


    如果您使用的是pandas,那么在pandas.io.json.normalize中隐藏了一个名为nested_to_record的函数,它可以做到这一点。

    1
    2
    3
    from pandas.io.json.normalize import nested_to_record    

    flat = nested_to_record(my_dict, sep='_')


    这不限于字典,而是实现.items()的每个映射类型。更进一步是更快,因为它避免了一个if条件。然而,学分归伊姆兰:

    1
    2
    3
    4
    5
    6
    7
    8
    def flatten(d, parent_key=''):
        items = []
        for k, v in d.items():
            try:
                items.extend(flatten(v, '%s%s_' % (parent_key, k)).items())
            except AttributeError:
                items.append(('%s%s' % (parent_key, k), v))
        return dict(items)


    python3.5中的功能性和性能性解决方案如何?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    from functools import reduce


    def _reducer(items, key, val, pref):
        if isinstance(val, dict):
            return {**items, **flatten(val, pref + key)}
        else:
            return {**items, pref + key: val}

    def flatten(d, pref=''):
        return(reduce(
            lambda new_d, kv: _reducer(new_d, *kv, pref),
            d.items(),
            {}
        ))

    这甚至更具表现力:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def flatten(d, pref=''):
        return(reduce(
            lambda new_d, kv: \
                isinstance(kv[1], dict) and \
                {**new_d, **flatten(kv[1], pref + kv[0])} or \
                {**new_d, pref + kv[0]: kv[1]},
            d.items(),
            {}
        ))

    使用中:

    1
    2
    3
    4
    my_obj = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y': 10}}, 'd': [1, 2, 3]}

    print(flatten(my_obj))
    # {'d': [1, 2, 3], 'cby': 10, 'cbx': 5, 'ca': 2, 'a': 1}


    使用生成器的python 3.3解决方案:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    def flattenit(pyobj, keystring=''):
       if type(pyobj) is dict:
         if (type(pyobj) is dict):
             keystring = keystring +"_" if keystring else keystring
             for k in pyobj:
                 yield from flattenit(pyobj[k], keystring + k)
         elif (type(pyobj) is list):
             for lelm in pyobj:
                 yield from flatten(lelm, keystring)
       else:
          yield keystring, pyobj

    my_obj = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y': 10}}, 'd': [1, 2, 3]}

    #your flattened dictionary object
    flattened={k:v for k,v in flattenit(my_obj)}
    print(flattened)

    # result: {'c_b_y': 10, 'd': [1, 2, 3], 'c_a': 2, 'a': 1, 'c_b_x': 5}


    扁平嵌套字典的简单函数。对于python 3,用.items()替换.iteritems()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    def flatten_dict(init_dict):
        res_dict = {}
        if type(init_dict) is not dict:
            return res_dict

        for k, v in init_dict.iteritems():
            if type(v) == dict:
                res_dict.update(flatten_dict(v))
            else:
                res_dict[k] = v

        return res_dict

    想法/要求是:获取不保留父键的平面字典。

    使用示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    dd = {'a': 3,
          'b': {'c': 4, 'd': 5},
          'e': {'f':
                     {'g': 1, 'h': 2}
               },
          'i': 9,
         }

    flatten_dict(dd)

    >> {'a': 3, 'c': 4, 'd': 5, 'g': 1, 'h': 2, 'i': 9}

    保存父键也很简单。


    这与伊姆兰和罗卢的回答相似。它不使用生成器,而是使用带有闭包的递归:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    def flatten_dict(d, separator='_'):
      final = {}
      def _flatten_dict(obj, parent_keys=[]):
        for k, v in obj.iteritems():
          if isinstance(v, dict):
            _flatten_dict(v, parent_keys + [k])
          else:
            key = separator.join(parent_keys + [k])
            final[key] = v
      _flatten_dict(d)
      return final

    >>> print flatten_dict({'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]})
    {'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}


    当嵌套dict还包含dict列表时,davoud的解决方案非常好,但没有给出令人满意的结果,但是他的代码适用于这种情况:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    def flatten_dict(d):
        items = []
        for k, v in d.items():
            try:
                if (type(v)==type([])):
                    for l in v: items.extend(flatten_dict(l).items())
                else:
                    items.extend(flatten_dict(v).items())
            except AttributeError:
                items.append((k, v))
        return dict(items)


    这是一个优雅的就地替换算法。用python 2.7和python 3.5测试。使用点字符作为分隔符。

    1
    2
    3
    4
    5
    6
    7
    8
    def flatten_json(json):
        if type(json) == dict:
            for k, v in list(json.items()):
                if type(v) == dict:
                    flatten_json(v)
                    json.pop(k)
                    for k2, v2 in v.items():
                        json[k+"."+k2] = v2

    例子:

    1
    2
    3
    4
    5
    d = {'a': {'b': 'c'}}                  
    flatten_json(d)
    print(d)
    unflatten_json(d)
    print(d)

    输出:

    1
    2
    {'a.b': 'c'}
    {'a': {'b': 'c'}}

    我在这里发布了这段代码以及匹配的unflatten_json函数。


    上面的答案非常有效。我只是想加上我写的unfratten函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def unflatten(d):
        ud = {}
        for k, v in d.items():
            context = ud
            for sub_key in k.split('_')[:-1]:
                if sub_key not in context:
                    context[sub_key] = {}
                context = context[sub_key]
            context[k.split('_')[-1]] = v
        return ud

    注意:这并不能解释已经存在于键中的"uu",就像扁平的对应键一样。


    如果您想要平面嵌套字典和所有唯一键列表,那么下面是解决方案:

    1
    2
    3
    4
    5
    6
    7
    def flat_dict_return_unique_key(data, unique_keys=set()):
        if isinstance(data, dict):
            [unique_keys.add(i) for i in data.keys()]
            for each_v in data.values():
                if isinstance(each_v, dict):
                    flat_dict_return_unique_key(each_v, unique_keys)
        return list(set(unique_keys))


    在简单的嵌套列表(如递归)中使用dict.popItem():

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    def flatten(d):
        if d == {}:
            return d
        else:
            k,v = d.popitem()
            if (dict != type(v)):
                return {k:v, **flatten(d)}
            else:
                flat_kv = flatten(v)
                for k1 in list(flat_kv.keys()):
                    flat_kv[k + '_' + k1] = flat_kv[k1]
                    del flat_kv[k1]
                return {**flat_kv, **flatten(d)}


    使用发电机:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    def flat_dic_helper(prepand,d):
        if len(prepand) > 0:
            prepand = prepand +"_"
        for k in d:
            i=d[k]
            if type(i).__name__=='dict':
                r = flat_dic_helper(prepand+k,i)
                for j in r:
                    yield j
            else:
                yield (prepand+k,i)

    def flat_dic(d): return dict(flat_dic_helper("",d))

    d={'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]}
    print(flat_dic(d))


    >> {'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    def flatten(unflattened_dict, separator='_'):
        flattened_dict = {}

        for k, v in unflattened_dict.items():
            if isinstance(v, dict):
                sub_flattened_dict = flatten(v, separator)
                for k2, v2 in sub_flattened_dict.items():
                    flattened_dict[k + separator + k2] = v2
            else:
                flattened_dict[k] = v

        return flattened_dict


    我总是喜欢通过.items()访问dict对象,因此为了扁平化dict,我使用以下递归生成器flat_items(d)。如果你想再来一次dict,就这样包装:flat = dict(flat_items(d))

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    def flat_items(d, key_separator='.'):
       """
        Flattens the dictionary containing other dictionaries like here: https://stackoverflow.com/questions/6027558/flatten-nested-python-dictionaries-compressing-keys

        >>> example = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]}
        >>> flat = dict(flat_items(example, key_separator='_'))
        >>> assert flat['c_b_y'] == 10
       """

        for k, v in d.items():
            if type(v) is dict:
                for k1, v1 in flat_items(v, key_separator=key_separator):
                    yield key_separator.join((k, k1)), v1
            else:
                yield k, v