展平嵌套的Python词典,压缩键

A T*_*mes 147 python dictionary

假设你有一个字典,如:

{'a': 1,
 'c': {'a': 2,
       'b': {'x': 5,
             'y' : 10}},
 'd': [1, 2, 3]}
Run Code Online (Sandbox Code Playgroud)

你会如何将其扁平化为:

{'a': 1,
 'c_a': 2,
 'c_b_x': 5,
 'c_b_y': 10,
 'd': [1, 2, 3]}
Run Code Online (Sandbox Code Playgroud)

Imr*_*ran 193

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

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}
Run Code Online (Sandbox Code Playgroud)

  • 如果用`try..except`块替换`isinstance`,这将适用于任何映射,即使它不是从`dict`派生的. (5认同)
  • 如果你想要在扁平版本中保存空字典,你可能想要改变`if isinstance(v,collections.MutableMapping):`到`if v和isinstance(v,collections.MutableMapping):` (4认同)
  • 注意,`new_key = parent_key + sep + k,如果parent_key else k`假定键始终是字符串,否则将引发`TypeError:无法连接'str'和[other]对象。但是,您可以通过简单地将`k`强制转换为字符串(`str(k)`),或将键串联到一个元组而不是字符串中来解决此问题(元组也可以是dict键)。 (3认同)
  • 将其更改为测试 `collections.MutableMapping` 以使其更通用。但是对于 Python < 2.6,`try..except` 可能是最好的选择。 (2认同)
  • 回答了我自己的问题:我添加了一个“elif”,然后就成功了... `elif isinstance(v,list): for idx,val in enumerate(v): new_key = str(parent_key) + sep + str(k ) + sep + str(idx) if parent_key else str(k) + sep + str(idx) items.extend(Controller.flatten(v[idx],new_key,sep=sep).items())` (2认同)

nin*_*cko 59

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

  1. 有关键空间的问题吗?例如,{'a_b':{'c':1}, 'a':{'b_c':2}}会导致{'a_b_c':???}.以下解决方案通过返回可迭代的对来避免问题.
  2. 如果性能是一个问题,密钥减少器功能(我在此称为"加入")是否需要访问整个密钥路径,或者它是否只能在树中的每个节点上执行O(1)工作?如果你想说joinedKey = '_'.join(*keys),那将花费你O(N ^ 2)的运行时间.但是,如果你愿意说nextKey = previousKey+'_'+thisKey,这会让你获得O(N)时间.下面的解决方案允许您同时执行这两个操作(因为您只能连接所有键,然后对它们进行后处理).

(性能不太可能是一个问题,但我会详细说明其他人关心的第二点:在实现这一点时,有许多危险的选择.如果你递归地执行此操作并且产生并重新产生,或者任何等效的接触节点超过一次(这是很容易不小心做),你正在做的潜在O(N ^ 2)工作,而不是O(N).这是因为也许你正在计算的关键a,然后a_1a_1_i...,然后计算aa_1然后a_1_ii......,但真的是你不应该计算a_1一次.即使你不重新计算它,重新得到它("级别由级"方法)是一样糟糕.一个很好的例子是考虑性能{1:{1:{1:{1:...(N times)...{1:SOME_LARGE_DICTIONARY_OF_SIZE_N}...}}}})

下面是我写的一个功能flattenDict(d, join=..., lift=...),它可以适应多种用途,可以做你想要的.遗憾的是,在不产生上述性能损失的情况下制作这个函数的懒惰版本相当困难(许多python内置函数如chain.from_iterable实际上并不高效,我只是在对此代码的三个不同版本进行大量测试之后才意识到这一点.这个).

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
Run Code Online (Sandbox Code Playgroud)

为了更好地理解正在发生的事情,下面是不熟悉reduce(左)的人的图表,也称为"向左折叠".有时用初始值代替k0绘制(不是列表的一部分,传递给函数).在这里,J是我们的join功能.我们每次进行预处理ķ ñlift(k).

               [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
Run Code Online (Sandbox Code Playgroud)

这实际上是相同的functools.reduce,但是我们的函数对树的所有关键路径执行此操作.

>>> reduce(lambda a,b:(a,b), range(5))
((((0, 1), 2), 3), 4)
Run Code Online (Sandbox Code Playgroud)

演示(我以其他方式放入docstring):

>>> 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}
Run Code Online (Sandbox Code Playgroud)

性能:

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
Run Code Online (Sandbox Code Playgroud)

......感叹,不要以为那是我的错......


[由于节制问题导致的不重要的历史记录]

关于Flatten的一个字典词典(2级深度)在Python中被称为复制:

这个问题的解决方案可以通过这样做来实现sorted( sum(flatten(...),[]) ).反向是不可能的:虽然这是事实,所述flatten(...)可以从重复的指称通过映射高阶累加器被回收,一个不能恢复键.(编辑:事实证明,所谓的重复所有者的问题是完全不同的,因为它只处理2级深度的字典,尽管该页面上的一个答案给出了一般解决方案.)

  • 我不确定这是否与问题有关。此解决方案不会拼合词典列表的字典项,即{'a':[{'aa':1},{'ab':2}]}。可以轻松更改flattenDict函数以适应这种情况。 (2认同)

MYG*_*YGz 37

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

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])
Run Code Online (Sandbox Code Playgroud)

输出:

{'a': 1, 'c_a': 2, 'c_b_x': 5, 'c_b_y': 10, 'd': [1, 2, 3]}
Run Code Online (Sandbox Code Playgroud)

  • 这已被弃用,最新的是:`df = pd.json_normalize(d, sep='_')` (6认同)
  • 有点遗憾它不处理列表:) (5认同)
  • 或者只是传递sep参数:) (4认同)
  • @MohammadYusuf 我无法仅使用“json_normalize”函数中的参数将键转换为字符串。它内置于 JSON 端。也许,他们将来会改变它。它仍然是一款紧凑的单行键盘,适合标准的字符串键。 (2认同)

div*_*ero 26

这是一种"功能","一线"的实现.它是递归的,基于条件表达式和字典理解.

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 }
Run Code Online (Sandbox Code Playgroud)

测试:

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}
Run Code Online (Sandbox Code Playgroud)

  • 我很担心,没有发现使用递归的答案。这些天我们的年轻人怎么了? (2认同)

Aar*_*ock 19

如果您正在使用pandas,则在pandas.io.json._normalize被调用nested_to_record中隐藏了一个功能,它可以完全执行此操作.

from pandas.io.json._normalize import nested_to_record    

flat = nested_to_record(my_dict, sep='_')
Run Code Online (Sandbox Code Playgroud)

  • 对我有用的是“from pandas.io.json._normalize importnested_to_record”。请注意“标准化”之前的下划线(“_”)。 (2认同)
  • @EyalLevin好收获!这在0.25.x中已更改,我已经更新了答案。:) (2认同)

Nik*_* VJ 13

不完全是 OP 所要求的,但是很多人来到这里寻找方法来扁平化现实世界的嵌套 JSON 数据,这些数据可以在数组中嵌套键值 json 对象和数组以及 json 对象等等。JSON 不包含元组,因此我们不必担心这些。

我找到了@roneo@Imran 发布答案的列表包含评论的实现

https://github.com/ScriptSmith/socialreaper/blob/master/socialreaper/tools.py#L8

import collections
def flatten(dictionary, parent_key=False, separator='.'):
    """
    Turn a nested dictionary into a flattened dictionary
    :param dictionary: The dictionary to flatten
    :param parent_key: The string to prepend to dictionary's keys
    :param separator: The string used to separate flattened keys
    :return: A flattened dictionary
    """

    items = []
    for key, value in dictionary.items():
        new_key = str(parent_key) + separator + key if parent_key else key
        if isinstance(value, collections.MutableMapping):
            items.extend(flatten(value, new_key, separator).items())
        elif isinstance(value, list):
            for k, v in enumerate(value):
                items.extend(flatten({str(k): v}, new_key).items())
        else:
            items.append((new_key, value))
    return dict(items)
Run Code Online (Sandbox Code Playgroud)

测试一下:

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

>> {'a': 1, 'c.a': 2, 'c.b.x': 5, 'c.b.y': 10, 'd.0': 1, 'd.1': 2, 'd.2': 3}
Run Code Online (Sandbox Code Playgroud)

这完成了我需要完成的工作:我把任何复杂的 json 扔在这个上面,它为我把它弄平了。

所有学分都归于 https://github.com/ScriptSmith

  • 到目前为止,这是我最喜欢的答案,因为它处理嵌套的字典列表。 (4认同)

Pav*_*ili 12

码:

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,''))
Run Code Online (Sandbox Code Playgroud)

结果:

$ python test.py
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}
Run Code Online (Sandbox Code Playgroud)

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


csa*_*nes 7

如果您是pythonic oneliners的粉丝:

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

list(pd.json_normalize(my_dict).T.to_dict().values())[0]
Run Code Online (Sandbox Code Playgroud)

返回:

{'a': 1, 'c.a': 2, 'c.b.x': 5, 'c.b.y': 10, 'd': [1, 2, 3]}
Run Code Online (Sandbox Code Playgroud)

[0]如果您有一个字典列表而不仅仅是一个字典,则可以从末尾开始。


Dav*_*jad 6

这不仅限于字典,而是每种映射类型.因为它避免了if条件,所以更快.然而,归功于伊姆兰:

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)
Run Code Online (Sandbox Code Playgroud)

  • 如果`d` 不是`dict` 而是一个没有实现`items` 的自定义映射类型,那么你的函数就会立即失败。因此,它不适用于所有映射类型,而仅适用于实现了“items()”的映射类型。 (2认同)
  • @ user6037143,不,根据定义,如果项目未实现,则它不是映射类型。 (2认同)

Rot*_*eti 6

Python3.5中的功能和高性能解决方案怎么样?

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(), 
        {}
    ))
Run Code Online (Sandbox Code Playgroud)

这更加高效:

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(), 
        {}
    ))
Run Code Online (Sandbox Code Playgroud)

正在使用:

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}
Run Code Online (Sandbox Code Playgroud)

  • 一个可读且有效的解决方案怎么样?;) 你在哪个版本上测试过这个?在 Python 3.4.3 中尝试此操作时出现“语法错误”。似乎“**all”的用法不合法。 (2认同)

Jak*_*kov 6

利用递归,保持简单和人类可读:

def flatten_dict(dictionary, accumulator=None, parent_key=None, separator="."):
    if accumulator is None:
        accumulator = {}

    for k, v in dictionary.items():
        k = f"{parent_key}{separator}{k}" if parent_key else k
        if isinstance(v, dict):
            flatten_dict(dictionary=v, accumulator=accumulator, parent_key=k)
            continue

        accumulator[k] = v

    return accumulator
Run Code Online (Sandbox Code Playgroud)

调用很简单:

new_dict = flatten_dict(dictionary)
Run Code Online (Sandbox Code Playgroud)

或者

new_dict = flatten_dict(dictionary, separator="_")
Run Code Online (Sandbox Code Playgroud)

如果我们想更改默认分隔符。

一点分解:

当函数第一次被调用时,它被调用只传递dictionary我们想要展平的。该accumulator参数是在这里支持递归,我们将在后面看到。因此,我们实例化为accumulator一个空字典,我们将在其中放置原始dictionary.

if accumulator is None:
    accumulator = {}
Run Code Online (Sandbox Code Playgroud)

当我们迭代字典的值时,我们为每个值构造一个键。该parent_key参数将是None第一个呼叫,而对于每一个嵌套的字典,它会包含指向它的关键,所以我们前置一个关键。

k = f"{parent_key}{separator}{k}" if parent_key else k
Run Code Online (Sandbox Code Playgroud)

如果vk指向的值是字典,则函数调用自身,传递嵌套字典accumulator(通过引用传递,因此对它所做的所有更改都在同一个实例上完成)和键,k以便我们可以构造连接键。注意continue声明。我们想跳过if块外的下一行,这样嵌套字典就不会出现在accumulatorunder key 中k

if isinstance(v, dict):
    flatten_dict(dict=v, accumulator=accumulator, parent_key=k)
    continue
Run Code Online (Sandbox Code Playgroud)

那么,如果值v不是字典,我们该怎么办?只需将其放在accumulator.

accumulator[k] = v
Run Code Online (Sandbox Code Playgroud)

完成后,我们只需返回accumulator,保持原始dictionary参数不变。

笔记

这仅适用于以字符串作为键的字典。它将与实现该__repr__方法的可散列对象一起使用,但会产生不需要的结果。


小智 5

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

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}
Run Code Online (Sandbox Code Playgroud)


Mat*_*yer 5

这是使用堆栈的解决方案。没有递归。

def flatten_nested_dict(nested):
    stack = list(nested.items())
    ans = {}
    while stack:
        key, val = stack.pop()
        if isinstance(val, dict):
            for sub_key, sub_val in val.items():
                stack.append((f"{key}_{sub_key}", sub_val))
        else:
            ans[key] = val
    return ans
Run Code Online (Sandbox Code Playgroud)