Supposons que vous ayez un dictionnaire comme:
{'a': 1,
'c': {'a': 2,
'b': {'x': 5,
'y' : 10}},
'd': [1, 2, 3]}
Comment voulez-vous résoudre le problème en quelque chose comme:
{'a': 1,
'c_a': 2,
'c_b_x': 5,
'c_b_y': 10,
'd': [1, 2, 3]}
Essentiellement, de la même manière que vous réduisez une liste imbriquée, vous devez simplement effectuer un travail supplémentaire pour itérer le dict par clé/valeur, créer de nouvelles clés pour votre nouveau dictionnaire et créer le dictionnaire à l'étape finale.
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}
L'affiche originale doit tenir compte de deux grandes considérations:
{'a_b':{'c':1}, 'a':{'b_c':2}}
Donnerait {'a_b_c':???}
. La solution ci-dessous élude le problème en renvoyant un nombre itérable de paires.joinedKey = '_'.join(*keys)
, cela vous coûtera O (N ^ 2) temps d'exécution, mais si vous êtes prêt à dire nextKey = previousKey+'_'+thisKey
, cela vous donne O(N) temps. La solution ci-dessous vous permet de faire les deux (puisque vous pouvez simplement concaténer toutes les clés, puis les post-traiter).(Les performances ne sont probablement pas un problème, mais je développerai le deuxième point au cas où quelqu'un d’autre serait intéressé: il existe de nombreux choix dangereux dans la mise en œuvre. Si vous le faites de manière récursive et que vous cédez et redonnez, ou n'importe quoi équivalent qui touche plusieurs fois les nœuds (ce qui est assez facile à faire accidentellement), vous effectuez potentiellement un travail O (N ^ 2) plutôt que O (N). C’est peut-être parce que vous calculez une clé a
alors a_1
Puis a_1_i
..., puis vous calculez a
puis a_1
Puis a_1_ii
..., mais vous ne devriez vraiment pas avoir à calculer à nouveau a_1
. Même si vous ne le recalculez pas, le renvoyer (une approche "niveau par niveau") est tout aussi mauvais. Un bon exemple est de penser à la performance sur {1:{1:{1:{1:...(N times)...{1:SOME_LARGE_DICTIONARY_OF_SIZE_N}...}}}}
)
Ci-dessous, une fonction que j’ai écrite flattenDict(d, join=..., lift=...)
, qui peut être adaptée à de nombreuses fins et peut faire ce que vous voulez. Malheureusement, il est assez difficile de faire une version paresseuse de cette fonction sans encourir les pénalités de performance ci-dessus (beaucoup de python comme trois versions différentes de ce code avant de choisir celui-ci).
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
Pour mieux comprendre ce qui se passe, vous trouverez ci-dessous un diagramme pour ceux qui ne connaissent pas reduce
(à gauche), aussi connu sous le nom de "pli à gauche". Parfois, il est dessiné avec une valeur initiale à la place de k0 (ne faisant pas partie de la liste, passée dans la fonction). Ici, J
est notre fonction join
. Nous pré-traitons chaque kn avec 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
C'est en fait la même chose que functools.reduce
, Mais notre fonction le fait pour tous les chemins de clé de l'arbre.
>>> reduce(lambda a,b:(a,b), range(5))
((((0, 1), 2), 3), 4)
Démonstration (que je mettrais sinon dans 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}
Performance:
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
... soupir, ne pense pas que c'est de ma faute ...
[note historique sans importance en raison de problèmes de modération]
Concernant le duplicata présumé de Aplatir un dictionnaire de dictionnaires (2 niveaux de profondeur) de listes en Python :
La solution de cette question peut être implémentée par rapport à celle-ci en faisant sorted( sum(flatten(...),[]) )
. L’inverse n’est pas possible: alors qu’il est vrai que les valeurs de flatten(...)
peuvent être récupérées du duplicata présumé en mappant un -accumulateur, on ne peut pas récupérer les clés. (edit: il s'avère également que la question du prétendu propriétaire de duplicata est complètement différente, en ce sens qu'elle ne traite que des dictionnaires avec une profondeur de 2 niveaux exactement, bien qu'une des réponses sur cette page donne une solution générale.)
Ou si vous utilisez déjà des pandas, vous pouvez le faire avec json_normalize()
comme suit:
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])
Sortie:
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'c_b_y': 10, 'd': [1, 2, 3]}
Voici une sorte d'implémentation "fonctionnelle", "one-liner". Il est récursif et basé sur une expression conditionnelle et une compréhension dictée.
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 }
Tester:
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}
Si vous utilisez pandas
, une fonction est cachée dans pandas.io.json.normalize
appelé nested_to_record
qui fait cela exactement.
from pandas.io.json.normalize import nested_to_record
flat = nested_to_record(my_dict, sep='_')
Code:
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,''))
Résultats:
$ python test.py
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}
J'utilise python3.2, mise à jour pour votre version de python.
Cela ne se limite pas aux dictionnaires, mais à tous les types de mappage qui implémentent .items (). En outre, il est plus rapide car il évite une condition if. Néanmoins, les crédits vont à 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)
Mon Python 3.3 Solution utilisant des générateurs:
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}
Fonction simple pour aplatir les dictionnaires imbriqués. Pour Python 3, remplacez .iteritems()
par .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
L'idée/exigence était la suivante: obtenir des dictionnaires à plat sans conserver les clés parent.
Exemple d'utilisation:
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}
Garder les clés parentales est également simple.
Que diriez-vous d'une solution fonctionnelle et performante 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(),
{}
))
C'est encore plus performant:
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(),
{}
))
Utilisé:
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}
Ceci est similaire à la réponse d'imran et de ralu. Il n'utilise pas de générateur, mais utilise la récursivité avec une fermeture:
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 solution de Davoud est très gentille mais ne donne pas de résultats satisfaisants lorsque le dict imbriqué contient également des listes de dict, mais que son code soit adapté à ce cas:
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)
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
Si vous voulez mettre à plat le dictionnaire imbriqué et que vous voulez une liste de clés uniques, alors voici la solution:
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))
Les réponses ci-dessus fonctionnent vraiment bien. Je pensais juste que j'ajouterais la fonction unflatten que j'ai écrite:
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
Remarque: cela ne représente pas le '_' déjà présent dans les clés, un peu comme les contreparties aplaties.
Voici un algorithme pour un remplacement élégant et en place. Testé avec Python 2.7 et Python 3.5. Utilisation du caractère à points comme séparateur.
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
Exemple:
d = {'a': {'b': 'c'}}
flatten_json(d)
print(d)
unflatten_json(d)
print(d)
Sortie:
{'a.b': 'c'}
{'a': {'b': 'c'}}
J'ai publié ce code ici avec le correspondant unflatten_json
une fonction.
Utilisation de générateurs:
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}
Utilisation de dict.popitem () dans une récursion directe semblable à une liste imbriquée:
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)}
Je préfère toujours accéder aux objets dict
via .items()
, aussi, pour aplatir les dictures, j'utilise le générateur récursif suivant flat_items(d)
. Si vous aimez avoir à nouveau dict
, enveloppez-le simplement comme ceci: 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