Il y a deux grandes considérations que l'affiche originale doit prendre en compte :
- Y a-t-il des problèmes d'encombrement du clavier ? Par exemple,
{'a_b':{'c':1}, 'a':{'b_c':2}}
aurait pour conséquence {'a_b_c':???}
. La solution ci-dessous contourne le problème en retournant un itérable de paires.
- Si les performances sont un problème, la fonction de réduction des clés (que j'appellerai par la suite "join") doit-elle avoir accès à l'ensemble du chemin des clés, ou peut-elle simplement effectuer un travail O(1) à chaque nœud de l'arbre ? Si vous voulez être capable de dire
joinedKey = '_'.join(*keys)
qui vous coûtera O(N^2) en temps de fonctionnement. Cependant, si vous êtes prêt à dire nextKey = previousKey+'_'+thisKey
ce qui vous donne un temps O(N). La solution ci-dessous vous permet de faire les deux (puisque vous pourriez simplement concaténer toutes les clés, puis les post-traiter).
(Les performances ne sont probablement pas un problème, mais je vais m'étendre sur le second point au cas où cela intéresserait quelqu'un d'autre : Dans la mise en œuvre de ce système, il existe de nombreux choix dangereux. Si vous faites cela de manière récursive et que vous céder et re-céder, ou tout ce qui est qui touche les nœuds plus d'une fois (ce qui est assez facile à faire accidentellement), vous effectuez potentiellement O(N^2) plutôt que O(N). Ceci est dû au fait que vous calculez peut-être une clé a
puis a_1
puis a_1_i
..., puis en calculant a
puis a_1
puis a_1_ii
..., mais vous ne devriez vraiment pas avoir à calculer a_1
encore. Même si vous ne le recalculez pas, le redonner (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}...}}}}
)
Voici une fonction que j'ai écrite flattenDict(d, join=..., lift=...)
qui peut être adapté à de nombreux usages 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 mentionnées ci-dessus (de nombreux buildins python comme chain.from_iterable ne sont pas vraiment efficaces, ce que je n'ai réalisé qu'après avoir testé trois versions différentes de ce code avant de me décider pour celle-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
Afin de mieux comprendre ce qui se passe, voici un diagramme pour ceux qui ne sont pas familiers avec le système d'alerte précoce. reduce
(gauche), autrement appelé "pli gauche". Parfois, il est dessiné avec une valeur initiale à la place de k0 (qui ne fait pas partie de la liste, passée dans la fonction). Ici, J
est notre join
fonction. Nous pré-traitons chaque 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
C'est en fait la même chose que functools.reduce
mais où notre fonction le fait pour tous les chemins-clés de l'arbre.
>>> reduce(lambda a,b:(a,b), range(5))
((((0, 1), 2), 3), 4)
Démonstration (que j'aurais autrement mis dans la docstring) :
>>> 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}
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, je ne pense pas que celle-là soit de ma faute...
[note historique sans importance en raison de problèmes de modération].
En ce qui concerne le prétendu double emploi de Aplatir un dictionnaire de dictionnaires (2 niveaux de profondeur) de listes
La solution de cette question peut être implémentée en termes de celle-ci en faisant sorted( sum(flatten(...),[]) )
. L'inverse n'est pas possible : s'il est vrai que la valeurs de flatten(...)
peut être récupéré à partir du prétendu doublon en mappant un accumulateur d'ordre supérieur, on ne peut pas récupérer les clés. (edit : Il s'avère également que la question du propriétaire du prétendu duplicata est complètement différente, en ce sens qu'elle ne traite que des dictionnaires d'une profondeur d'exactement 2 niveaux, bien qu'une des réponses sur cette page donne une solution générale).
7 votes
Il existe également une bibliothèque pour cela : github.com/ianlini/flatten-dict
0 votes
voir aussi : stackoverflow.com/questions/14692690
0 votes
Je vois des performances très différentes pour les approches proposées dans les réponses.
0 votes
La question devrait avoir à la fin : "de sorte que les clés de tous les niveaux sur le chemin de la feuille soient concaténées ?" Ou changer l'en-tête en "compression (= concaténation) des clés". Il devrait y avoir "concaténation" dans la question pour ceux qui cherchent. Je cherchais une solution qui donnerait une liste des clés sur le chemin de la feuille, et non une concaténation. Vous pourriez dire utiliser
split()
alors, mais il existe d'autres moyens directs que cette question n'encourage pas.