原发帖者需要考虑两个大的考虑因素:
- 是否存在键空间破坏问题?例如,
{'a_b':{'c':1}, 'a':{'b_c':2}} 将导致 {'a_b_c':???}。以下解决方案通过返回可迭代的对来规避问题。
- 如果性能是一个问题,key-reducer 功能(我在此称为“join”)是否需要访问整个 key-path,或者它是否可以在每个节点上执行 O(1) 工作?树?如果你想能够说
joinedKey = '_'.join(*keys),那将花费你 O(N^2) 的运行时间。但是,如果您愿意说nextKey = previousKey+'_'+thisKey,那么您将获得 O(N) 时间。下面的解决方案让您可以同时执行这两种操作(因为您可以仅连接所有键,然后对它们进行后处理)。
(性能可能不是问题,但我会详细说明第二点,以防其他人关心:在实现这一点时,有许多危险的选择。如果你递归地执行此操作并产生并重新产生,或者 anything 等价于多次接触节点(这很容易意外地做到),你正在做潜在的 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 内置函数实际上效率不高,我只是在对这段代码的三个不同版本进行广泛测试之后才意识到这一点,然后才确定)这个)。
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
为了更好地理解发生了什么,下图是为不熟悉reduce(左)的人准备的,也称为“向左折叠”。有时它是用初始值代替 k0 绘制的(不是列表的一部分,传递给函数)。这里,J 是我们的 join 函数。我们用lift(k)预处理每个kn。
[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 相同,但我们的函数对树的所有关键路径都执行此操作。
>>> reduce(lambda a,b:(a,b), range(5))
((((0, 1), 2), 3), 4)
演示(否则我会放在文档字符串中):
>>> testData = {
'a':1,
'b':2,
'c':{
'aa':11,
'bb':22,
'cc':{
'aaa':111
}
}
}
from pprint import pprint as pp
>>> pp(dict( flattenDict(testData) ))
{('a',): 1,
('b',): 2,
('c', 'aa'): 11,
('c', 'bb'): 22,
('c', 'cc', 'aaa'): 111}
>>> pp(dict( flattenDict(testData, join=lambda a,b:a+'_'+b, lift=lambda x:x) ))
{'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}
性能:
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
……唉,别以为是我的错……
[由于审核问题,不重要的历史记录]
关于涉嫌重复Flatten a dictionary of dictionaries (2 levels deep) of lists
这个问题的解决方案可以通过sorted( sum(flatten(...),[]) )来实现。反过来是不可能的:虽然flatten(...) 的值 确实可以通过映射一个高阶累加器从所谓的重复中恢复,但无法恢复键。 (编辑:事实证明,所谓的重复所有者的问题是完全不同的,因为它只处理精确到 2 级深度的字典,尽管该页面上的一个答案给出了一个通用的解决方案。)