拼合嵌套的Python字典,压缩键

假设你有一个字典,如:

{'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': [1, 2, 3]} 

基本上,你将扁平化一个嵌套列表的方式相同,你只需要做一些额外的工作,通过键/值迭代字典,为你的新字典创build新的键,并在最后一步创build字典。

 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} 

原始海报需要考虑的两大考虑因素是:

  1. 是否有键空间破坏问题? 例如, {'a_b':{'c':1}, 'a':{'b_c':2}}将导致{'a_b_c':???} 。 下面的解决scheme通过返回可迭代的对来回避这个问题。
  2. 如果性能问题,key-reducer函数(我称之为“join”)是否需要访问整个关键path,还是只能在树的每个节点上执行O(1)? 如果你想能够说joinedKey = '_'.join(*keys) ,这将花费你O(N ^ 2)的运行时间。 然而,如果你愿意说nextKey = previousKey+'_'+thisKey ,那么你就得到了O(N)的时间。 下面的解决scheme可以让你做这两个(因为你可以只是连接所有的关键,然后后处理)。

(性能不太可能是一个问题,但是我会详细说明其他人关心的第二点:在实现这个过程中,有很多危险的select,如果你recursion地做这个,并且屈服和重新屈服,节点不止一次(这很容易意外地做),你正在做O(N ^ 2)的工作,而不是O(N),这是因为也许你正在计算一个关键字a然后是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=...)的函数flattenDict(d, join=..., lift=...)它可以适应多种用途,可以做你想做的事情。 遗憾的是,在不产生上述性能损失的情况下制作这个函数的懒惰版本是相当困难的(像chain.from_iterable之类的许多python内置函数实际上并不高效,我只有在对这个代码的三个不同版本进行广泛testing之后才意识到,这个)。

 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)预处理每个k n

  [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 ,但是我们的函数在树的所有关键path中都这样做。

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

示范(我否则把它放在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} 

性能:

 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 

感叹,不要以为是我的错


[由于pipe理问题引起的不重要的历史logging]

关于扁平化Python的列表中的字典(2级深)的所谓重复:

这个问题的解决scheme可以通过执行sorted( sum(flatten(...),[]) ) 。 反过来是不可能的:虽然可以通过映射更高阶的累加器来从所谓的重复中恢复flatten(...) ,但是不能恢复密钥。 (编辑:另外,事实certificate,所谓的重复所有者的问题是完全不同的,因为它只处理字典深度2层,尽pipe其中的一个答案给出了一个通用的解决scheme。)

这是一种“function性”,“一线式”的实施。 它是recursion的,基于条件expression式和字典理解。

 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 } 

testing:

 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} 

码:

 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,'')) 

结果:

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

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

这不限于字典,而是每种映射types。 进一步更快,因为它避免了条件。 尽pipe如此,信贷去伊姆兰:

 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) 

这与imran和ralu的答案类似。 它不使用生成器,而是使用闭包recursion:

 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} 

或者,如果您已经在使用pandas,可以使用json_normalize()来做到这一点:

 import pandas as pd df = pd.io.json.json_normalize({'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]}) print(df.to_dict(orient='records')[0]) 

输出:

 {'a': 1, 'c.a': 2, 'cbx': 5, 'cby': 10, 'd': [1, 2, 3]} 

如果你不想要. 分隔符,你可以像这样破解它:

 d = df.to_dict(orient='records')[0] print dict(zip([x.replace('.', '_') for x in d.keys()], d.values())) 

输出:

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

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

 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 #your flattened dictionary object flattened={k:v for k,v in flattenit(person)} 

达沃德的解决scheme是非常好的,但不能给予满意的结果,当嵌套字典也包含字典列表,但他的代码适应于这种情况:

 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) 

这里有一个优雅的就地replacealgorithm。 用Python 2.7和Python 3.5testing。 使用点字符作为分隔符。

 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 

例:

 d = {'a': {'b': 'c'}} flatten_json(d) print(d) unflatten_json(d) print(d) 

输出:

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

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

如何在Python3中的function和性能的解决scheme?

 def flatten(dict_, pre=''): merge = lambda all, d: {pre + d[0]: d[1], **all} recurse = lambda all, d: type(d[1]) == dict and {**flatten(d[1], pre + d[0]), **all} return reduce(lambda all, d: recurse(all, d) or merge(all, d), dict_.items(), {}) 

正在使用:

 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} 

使用发电机:

 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} 

上面的答案真的很好。 只是想我会添加我写的unflatten函数:

 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 

注意:这不包含已经存在于键中的'_',就像平坦的对应物一样。

我试图扁平下面的json:

 { "blkio_stats": { "io_merged_recursive": [], "io_queue_recursive": [], "io_service_bytes_recursive": [], "io_service_time_recursive": [], "io_serviced_recursive": [], "io_time_recursive": [], "io_wait_time_recursive": [], "sectors_recursive": [] }, "cpu_stats": { "cpu_usage": { "percpu_usage": [56477793484, 53845065668], "total_usage": 110322859152, "usage_in_kernelmode": 74310000000, "usage_in_usermode": 24760000000 }, "system_cpu_usage": 11583455420000000, "throttling_data": { "periods": 0, "throttled_periods": 0, "throttled_time": 0 } }, "memory_stats": { "failcnt": 0, "limit": 4074237952, "max_usage": 485281792, "stats": { "active_anon": 7823360, "active_file": 8192, "cache": 8192, "dirty": 0, "hierarchical_memory_limit": 9223372036854771712, "inactive_anon": 0, "inactive_file": 0, "mapped_file": 0, "pgfault": 5602509, "pgmajfault": 0, "pgpgin": 5421031, "pgpgout": 5429850, "rss": 7823360, "rss_huge": 6291456, "total_active_anon": 7823360, "total_active_file": 8192, "total_cache": 8192, "total_dirty": 0, "total_inactive_anon": 0, "total_inactive_file": 0, "total_mapped_file": 0, "total_pgfault": 5602509, "total_pgmajfault": 0, "total_pgpgin": 5421031, "total_pgpgout": 5429850, "total_rss": 7823360, "total_rss_huge": 6291456, "total_unevictable": 0, "total_writeback": 0, "unevictable": 0, "writeback": 0 }, "usage": 7831552 }, "networks": { "eth0": { "rx_bytes": 1512143070, "rx_dropped": 0, "rx_errors": 0, "rx_packets": 2605267, "tx_bytes": 1450105558, "tx_dropped": 0, "tx_errors": 0, "tx_packets": 2326940 } }, "pids_stats": {}, "precpu_stats": { "cpu_usage": { "percpu_usage": null, "total_usage": 0, "usage_in_kernelmode": 0, "usage_in_usermode": 0 }, "system_cpu_usage": 0, "throttling_data": { "periods": 0, "throttled_periods": 0, "throttled_time": 0 } }, "read": "2017-02-17T10:22:07.278903338Z" } 

最好的select,以平整该文件,与。 因为分离标记是这样的: https ://stackoverflow.com/a/41840603/5730361它只是省略了“pids_stats”:{},选项:

 { "blkio_stats.io_merged_recursive": [], "blkio_stats.io_queue_recursive": [], "blkio_stats.io_service_bytes_recursive": [], "blkio_stats.io_service_time_recursive": [], "blkio_stats.io_serviced_recursive": [], "blkio_stats.io_time_recursive": [], "blkio_stats.io_wait_time_recursive": [], "blkio_stats.sectors_recursive": [], "cpu_stats.cpu_usage.percpu_usage": [56477793484, 53845065668], "cpu_stats.cpu_usage.total_usage": 110322859152, "cpu_stats.cpu_usage.usage_in_kernelmode": 74310000000, "cpu_stats.cpu_usage.usage_in_usermode": 24760000000, "cpu_stats.system_cpu_usage": 11583455420000000, "cpu_stats.throttling_data.periods": 0, "cpu_stats.throttling_data.throttled_periods": 0, "cpu_stats.throttling_data.throttled_time": 0, "memory_stats.failcnt": 0, "memory_stats.limit": 4074237952, "memory_stats.max_usage": 485281792, "memory_stats.stats.active_anon": 7823360, "memory_stats.stats.active_file": 8192, "memory_stats.stats.cache": 8192, "memory_stats.stats.dirty": 0, "memory_stats.stats.hierarchical_memory_limit": 9223372036854771712, "memory_stats.stats.inactive_anon": 0, "memory_stats.stats.inactive_file": 0, "memory_stats.stats.mapped_file": 0, "memory_stats.stats.pgfault": 5602509, "memory_stats.stats.pgmajfault": 0, "memory_stats.stats.pgpgin": 5421031, "memory_stats.stats.pgpgout": 5429850, "memory_stats.stats.rss": 7823360, "memory_stats.stats.rss_huge": 6291456, "memory_stats.stats.total_active_anon": 7823360, "memory_stats.stats.total_active_file": 8192, "memory_stats.stats.total_cache": 8192, "memory_stats.stats.total_dirty": 0, "memory_stats.stats.total_inactive_anon": 0, "memory_stats.stats.total_inactive_file": 0, "memory_stats.stats.total_mapped_file": 0, "memory_stats.stats.total_pgfault": 5602509, "memory_stats.stats.total_pgmajfault": 0, "memory_stats.stats.total_pgpgin": 5421031, "memory_stats.stats.total_pgpgout": 5429850, "memory_stats.stats.total_rss": 7823360, "memory_stats.stats.total_rss_huge": 6291456, "memory_stats.stats.total_unevictable": 0, "memory_stats.stats.total_writeback": 0, "memory_stats.stats.unevictable": 0, "memory_stats.stats.writeback": 0, "memory_stats.usage": 7831552, "networks.eth0.rx_bytes": 1512143070, "networks.eth0.rx_dropped": 0, "networks.eth0.rx_errors": 0, "networks.eth0.rx_packets": 2605267, "networks.eth0.tx_bytes": 1450105558, "networks.eth0.tx_dropped": 0, "networks.eth0.tx_errors": 0, "networks.eth0.tx_packets": 2326940, "precpu_stats.cpu_usage.percpu_usage": null, "precpu_stats.cpu_usage.total_usage": 0, "precpu_stats.cpu_usage.usage_in_kernelmode": 0, "precpu_stats.cpu_usage.usage_in_usermode": 0, "precpu_stats.system_cpu_usage": 0, "precpu_stats.throttling_data.periods": 0, "precpu_stats.throttling_data.throttled_periods": 0, "precpu_stats.throttling_data.throttled_time": 0, "read": "2017-02-17T10:22:07.278903338Z" } 

就解压文件而言, 作为选项,我select拼合嵌套Python字典,压缩键 – 因为它recursion和完美的工作。 请注意,“pids_stats”:{}缺失,因为在上一步中丢失了。

 { "blkio_stats": { "io_merged_recursive": [], "io_queue_recursive": [], "io_service_bytes_recursive": [], "io_service_time_recursive": [], "io_serviced_recursive": [], "io_time_recursive": [], "io_wait_time_recursive": [], "sectors_recursive": [] }, "cpu_stats": { "cpu_usage": { "percpu_usage": [56477793484, 53845065668], "total_usage": 110322859152, "usage_in_kernelmode": 74310000000, "usage_in_usermode": 24760000000 }, "system_cpu_usage": 11583455420000000, "throttling_data": { "periods": 0, "throttled_periods": 0, "throttled_time": 0 } }, "memory_stats": { "failcnt": 0, "limit": 4074237952, "max_usage": 485281792, "stats": { "active_anon": 7823360, "active_file": 8192, "cache": 8192, "dirty": 0, "hierarchical_memory_limit": 9223372036854771712, "inactive_anon": 0, "inactive_file": 0, "mapped_file": 0, "pgfault": 5602509, "pgmajfault": 0, "pgpgin": 5421031, "pgpgout": 5429850, "rss": 7823360, "rss_huge": 6291456, "total_active_anon": 7823360, "total_active_file": 8192, "total_cache": 8192, "total_dirty": 0, "total_inactive_anon": 0, "total_inactive_file": 0, "total_mapped_file": 0, "total_pgfault": 5602509, "total_pgmajfault": 0, "total_pgpgin": 5421031, "total_pgpgout": 5429850, "total_rss": 7823360, "total_rss_huge": 6291456, "total_unevictable": 0, "total_writeback": 0, "unevictable": 0, "writeback": 0 }, "usage": 7831552 }, "networks": { "eth0": { "rx_bytes": 1512143070, "rx_dropped": 0, "rx_errors": 0, "rx_packets": 2605267, "tx_bytes": 1450105558, "tx_dropped": 0, "tx_errors": 0, "tx_packets": 2326940 } }, "precpu_stats": { "cpu_usage": { "percpu_usage": null, "total_usage": 0, "usage_in_kernelmode": 0, "usage_in_usermode": 0 }, "system_cpu_usage": 0, "throttling_data": { "periods": 0, "throttled_periods": 0, "throttled_time": 0 } }, "read": "2017-02-17T10:22:07.278903338Z" } 

简单的函数来扁平化嵌套字典:

 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 

这个想法/要求是:获取平坦的字典,没有父母的关键。

使用示例:

 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} 

保持父母键也很简单。