Aplanar diccionarios nesteds, comprimir claves

Supongamos que tienes un diccionario como:

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

¿Cómo harías para aplanar eso en algo como:

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

Básicamente, de la misma forma en que aplanaría una lista anidada, solo tiene que hacer el trabajo adicional para iterar el dictado por clave / valor, crear nuevas claves para su nuevo diccionario y crear el diccionario en el paso final.

 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} 

Hay dos grandes consideraciones que el cartel original debe considerar:

  1. ¿Hay problemas de espacio en el teclado? Por ejemplo, {'a_b':{'c':1}, 'a':{'b_c':2}} resultaría en {'a_b_c':???} . La siguiente solución evita el problema devolviendo un iterable de pares.
  2. Si el rendimiento es un problema, ¿la función de reductor de clave (a la que me refiero como ‘unirse’) requiere acceso a la ruta de acceso completa, o puede simplemente hacer que O (1) funcione en cada nodo del árbol? Si desea poder decir joinedKey = '_'.join(*keys) , le costará O (N ^ 2) el tiempo de ejecución. Sin embargo, si está dispuesto a decir nextKey = previousKey+'_'+thisKey , eso le da una O (N) de tiempo. La solución a continuación le permite hacer ambas cosas (ya que simplemente podría concatenar todas las claves y luego postprocesarlas).

(Es probable que el rendimiento no sea un problema, pero explicaré el segundo punto en caso de que a alguien más le importe: al implementar esto, existen numerosas opciones peligrosas. Si lo hace de forma recursiva y cede y re-cede, o algo equivalente que toque Nodos más de una vez (lo que es bastante fácil de hacer accidentalmente), está haciendo un trabajo O (N ^ 2) en lugar de O (N). Esto se debe a que tal vez esté calculando una clave a luego a_1 luego a_1_i …, y luego calculando a luego a_1 luego a_1_ii …, pero en realidad no debería tener que volver a calcular a_1 . Incluso si no lo está recalculando, volver a producirlo (un enfoque de ‘nivel por nivel’) es simplemente Un buen ejemplo es pensar en el rendimiento en {1:{1:{1:{1:...(N times)...{1:SOME_LARGE_DICTIONARY_OF_SIZE_N}...}}}} )

A continuación se muestra una función que escribí flattenDict(d, join=..., lift=...) que puede adaptarse a muchos propósitos y puede hacer lo que quiera. Lamentablemente, es bastante difícil hacer una versión perezosa de esta función sin incurrir en las penalizaciones de rendimiento anteriores (muchas de las incorporaciones de Python como chain.from_iterable no son realmente eficientes, lo cual me di cuenta después de una prueba exhaustiva de tres versiones diferentes de este código antes de decidirme éste).

 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 

Para entender mejor lo que está pasando, a continuación hay un diagtwig para aquellos que no están familiarizados con reduce (izquierda), también conocido como “pliegue a la izquierda”. A veces se dibuja con un valor inicial en lugar de k0 (que no forma parte de la lista, se pasa a la función). Aquí, J es nuestra función de join . Nosotros preprocesamos cada k n con 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 

De hecho, esto es lo mismo que functools.reduce , pero donde nuestra función hace esto a todas las rutas clave del árbol.

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

Demostración (que de otro modo pondría en 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} 

Actuación:

 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 

… suspiro, no creas que es culpa mía …


[Nota histórica sin importancia debido a problemas de moderación]

Con respecto al supuesto duplicado de Flatten un diccionario de diccionarios (2 niveles de profundidad) de listas en Python :

La solución de esa pregunta se puede implementar en términos de esta haciendo sorted( sum(flatten(...),[]) ) . Lo contrario no es posible: si bien es cierto que los valores de flatten(...) se pueden recuperar del supuesto duplicado al asignar un acumulador de orden superior, no se pueden recuperar las claves. (Editar: También resulta que la pregunta del supuesto propietario duplicado es completamente diferente, en el sentido de que solo trata con diccionarios de 2 niveles de profundidad, aunque una de las respuestas en esa página ofrece una solución general).

O si ya estás usando pandas, puedes hacerlo con json_normalize() así:

 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]) 

Salida:

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

Aquí hay una especie de implementación “funcional”, “de una sola línea”. Es recursivo y se basa en una expresión condicional y una comprensión del dict.

 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 } 

Prueba:

 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} 

Código:

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

Resultados:

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

Estoy usando python3.2, actualización para su versión de python.

Si está utilizando pandas hay una función oculta en pandas.io.json.normalize llamada nested_to_record que hace esto exactamente.

 from pandas.io.json.normalize import nested_to_record flat = nested_to_record(my_dict, sep='_') 

Esto no está restringido a los diccionarios, sino a cada tipo de mapeo que implemente .items (). Además es más rápido, ya que evita una condición if. Sin embargo los créditos van a Imran:

 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) 

¿Qué tal una solución funcional y de rendimiento en 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(), {} )) 

Esto es aún más importante:

 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(), {} )) 

En uso:

 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} 

Mi solución Python 3.3 usando generadores:

 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} 

Función simple para aplanar diccionarios nesteds. Para Python 3, reemplace .iteritems() con .items()

 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 

La idea / requerimiento era: obtener diccionarios planos sin guardar las claves de los padres.

Ejemplo de uso:

 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} 

Mantener las claves de los padres también es simple.

Esto es similar a la respuesta tanto de imran como de ralu. No utiliza un generador, sino que emplea la recursión con un cierre:

 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} 

La solución de Davoud es muy buena, pero no da resultados satisfactorios cuando el dict nested también contiene listas de dictados, pero su código se adapta para ese caso:

 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) 

Las respuestas anteriores funcionan muy bien. Solo pensé que agregaría la función no plana que escribí:

 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 

Nota: Esto no tiene en cuenta que ‘_’ ya está presente en las teclas, al igual que las contrapartes aplanadas.

Aquí está un algoritmo para el reemplazo elegante, en el lugar. Probado con Python 2.7 y Python 3.5. Usando el carácter de punto como separador.

 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 

Ejemplo:

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

Salida:

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

Publiqué este código aquí junto con la función unflatten_json correspondiente.

Si desea un diccionario nested y desea todas las claves únicas, esta es la solución:

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

Usando generadores:

 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} 

Usando dict.popitem () en una recursión similar a una lista anidada:

 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)} 
 def flatten(unflattened_dict, seperator='_'): flattened_dict = {} for k, v in unflattened_dict.items(): if isinstance(v, dict): sub_flattened_dict = flatten(v, "_") for k2, v2 in sub_flattened_dict.items(): flattened_dict[k+seperator+k2] = v2 else: flattened_dict[k] = v return flattened_dict 

Siempre prefiero acceder a los objetos dict través de .items() , así que para aplanar los dados, uso el siguiente generador recursivo flat_items(d) . Si te gusta tener dict nuevamente, simplemente envuélvelo así: flat = dict(flat_items(d))

 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