L'affiche originale doit prendre en compte deux grandes considérations:
- Y a-t-il des problèmes de clobber d'espace de clés? Par exemple,
{'a_b':{'c':1}, 'a':{'b_c':2}}
entraînerait {'a_b_c':???}
. La solution ci-dessous élude le problème en renvoyant un itérable de paires.
- Si les performances sont un problème, la fonction de réduction de clé (que j'appelle ici `` rejoindre '') nécessite-t-elle un accès à l'ensemble du chemin de clé, ou peut-elle simplement faire fonctionner O (1) à chaque nœud de l'arborescence? Si vous voulez pouvoir dire
joinedKey = '_'.join(*keys)
, cela vous coûtera O (N ^ 2) temps d'exécution. Cependant, si vous êtes prêt à le dire nextKey = previousKey+'_'+thisKey
, cela vous donne du temps O (N). La solution ci-dessous vous permet de faire les deux (puisque vous pouvez simplement concaténer toutes les clés, puis les post-traiter).
( La performance est peu probable un problème, mais je vais élaborer sur le deuxième point au cas où quelqu'un d' autre soins. Dans la mise en œuvre cela, il y a de nombreux choix dangereux Si vous le faites de manière récursive et le rendement et re-rendement, ou quoi que ce soit équivalent qui touche nœuds plus d'une fois ( ce qui est assez facile à faire par hasard), vous êtes en train de faire potentiellement O (N ^ 2) travail plutôt que O (N). en effet , vous êtes peut - être une clé calcules a
puis a_1
alors a_1_i
..., puis le calcul a
alors a_1
alors a_1_ii
..., mais en réalité, vous ne devriez pas avoir à calculer à a_1
nouveau. Même si vous ne le recalculez pas, le redonner (une approche "niveau par niveau") est tout aussi mauvais. Un bon exemple est 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ée à de nombreuses fins et qui peut faire ce que vous voulez. Malheureusement, il est assez difficile de créer une version paresseuse de cette fonction sans encourir les pénalités de performances ci-dessus (de nombreuses versions intégrées de python comme chain.from_iterable ne sont pas réellement efficaces, ce que je n'ai réalisé qu'après des tests approfondis de trois versions différentes de ce code avant de me décider celui-là).
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), autrement connu sous le nom de «repli à gauche». Parfois, il est dessiné avec une valeur initiale à la place de k0 (ne fait pas partie de la liste, passé dans la fonction). Voici J
notre join
fonction. Nous prétraitons chaque k n 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 où notre fonction le fait à 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 autrement 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 prétendu duplicata de Flatten un dictionnaire de dictionnaires (2 niveaux de profondeur) de listes en Python :
La solution de cette question peut être mise en œuvre en fonction de celle-ci en faisant sorted( sum(flatten(...),[]) )
. L'inverse n'est pas possible: s'il est vrai que les valeurs de flatten(...)
peuvent être récupérées à partir du prétendu doublon en mappant un accumulateur d'ordre supérieur, on ne peut pas récupérer les clés. (modifier: Il s'avère également que la question du prétendu propriétaire en double est complètement différente, en ce qu'elle ne traite que des dictionnaires exactement à 2 niveaux de profondeur, bien qu'une des réponses sur cette page donne une solution générale.)