Lorsque vous essayez de répondre à une telle question, vous devez vraiment donner les limites du code que vous proposez comme solution. S'il ne s'agissait que de performances, cela ne me dérangerait pas trop, mais la plupart des codes proposés comme solution (y compris la réponse acceptée) ne parviennent pas à aplatir une liste dont la profondeur est supérieure à 1000.
Quand je dis la plupart des codes, je veux dire tous les codes qui utilisent n'importe quelle forme de récursivité (ou appellent une fonction de bibliothèque standard qui est récursive). Tous ces codes échouent car pour chaque appel récursif effectué, la pile (d'appel) augmente d'une unité et la pile d'appel python (par défaut) a une taille de 1000.
Si vous n'êtes pas trop familier avec la pile d'appels, alors peut-être que ce qui suit vous aidera (sinon vous pouvez simplement faire défiler jusqu'à la mise en œuvre ).
Taille de la pile d'appels et programmation récursive (analogie avec les donjons)
Trouver le trésor et sortir
Imaginez que vous entrez dans un immense donjon avec des chambres numérotées , à la recherche d'un trésor. Vous ne connaissez pas l'endroit mais vous avez des indications sur la façon de trouver le trésor. Chaque indication est une énigme (la difficulté varie, mais vous ne pouvez pas prédire à quel point elles seront difficiles). Vous décidez de réfléchir un peu à une stratégie pour gagner du temps, vous faites deux constats:
- Il est difficile (long) de trouver le trésor car vous devrez résoudre des énigmes (potentiellement difficiles) pour y arriver.
- Une fois le trésor trouvé, le retour à l'entrée peut être facile, il vous suffit d'utiliser le même chemin dans l'autre sens (même si cela nécessite un peu de mémoire pour rappeler votre chemin).
En entrant dans le donjon, vous remarquez un petit cahier ici. Vous décidez de l'utiliser pour noter chaque pièce que vous quittez après avoir résolu une énigme (lorsque vous entrez dans une nouvelle pièce), de cette façon, vous pourrez retourner à l'entrée. C'est une idée géniale, vous ne dépenserez même pas un centime pour mettre en œuvre votre stratégie.
Vous entrez dans le donjon, résolvant avec grand succès les 1001 premières énigmes, mais voici quelque chose que vous n'aviez pas planifié, vous n'avez plus de place dans le cahier que vous avez emprunté. Vous décidez d' abandonner votre quête car vous préférez ne pas avoir le trésor que d'être perdu pour toujours à l'intérieur du donjon (cela a l'air intelligent en effet).
Exécuter un programme récursif
Fondamentalement, c'est exactement la même chose que de trouver le trésor. Le donjon est la mémoire de l' ordinateur , votre objectif n'est plus de trouver un trésor mais de calculer une fonction (trouver f (x) pour un x donné ). Les indications sont simplement des sous-routines qui vous aideront à résoudre f (x) . Votre stratégie est la même que la stratégie de pile d'appels , le bloc-notes est la pile, les salles sont les adresses de retour des fonctions:
x = ["over here", "am", "I"]
y = sorted(x) # You're about to enter a room named `sorted`, note down the current room address here so you can return back: 0x4004f4 (that room address looks weird)
# Seems like you went back from your quest using the return address 0x4004f4
# Let's see what you've collected
print(' '.join(y))
Le problème que vous avez rencontré dans le donjon sera le même ici, la pile d'appels a une taille finie (ici 1000) et donc, si vous entrez trop de fonctions sans revenir en arrière, vous remplirez la pile d'appels et aurez une erreur qui ressemblera comme « aventurier Cher, je suis désolé , mais votre ordinateur portable est pleine » : RecursionError: maximum recursion depth exceeded
. Notez que vous n'avez pas besoin de récursivité pour remplir la pile d'appels, mais il est très peu probable qu'un programme non récursif appelle 1000 fonctions sans jamais revenir. Il est important de comprendre également qu'une fois que vous êtes revenu d'une fonction, la pile d'appels est libérée de l'adresse utilisée (d'où le nom "pile", l'adresse de retour est poussée avant d'entrer dans une fonction et retirée lors du retour). Dans le cas particulier d'une récursion simple (une fonctionf
qui s'appelle lui-même encore et encore -) vous entrerez f
encore et encore jusqu'à ce que le calcul soit terminé (jusqu'à ce que le trésor soit trouvé) et reviendrez f
jusqu'à ce que vous retourniez à l'endroit où vous avez appelé f
en premier lieu. La pile d'appels ne sera jamais libérée de quoi que ce soit jusqu'à la fin où elle sera libérée de toutes les adresses de retour l'une après l'autre.
Comment éviter ce problème?
C'est en fait assez simple: "n'utilisez pas la récursivité si vous ne savez pas jusqu'où elle peut aller". Ce n'est pas toujours vrai car dans certains cas, la récursivité des appels de queue peut être optimisée (TCO) . Mais en python, ce n'est pas le cas, et même une fonction récursive "bien écrite" n'optimisera pas l' utilisation de la pile. Il y a un article intéressant de Guido sur cette question: Tail Recursion Elimination .
Il existe une technique que vous pouvez utiliser pour rendre toute fonction récursive itérative, cette technique que nous pourrions appeler apporter votre propre cahier . Par exemple, dans notre cas particulier, nous explorons simplement une liste, entrer dans une pièce équivaut à entrer une sous-liste, la question que vous devez vous poser est de savoir comment puis-je revenir d'une liste à sa liste parente? La réponse n'est pas si complexe, répétez ce qui suit jusqu'à ce que le stack
soit vide:
- pousser la liste actuelle
address
et index
dans un stack
lors de la saisie d'une nouvelle sous-liste (notez qu'une adresse de liste + index est également une adresse, nous utilisons donc exactement la même technique utilisée par la pile des appels);
- chaque fois qu'un élément est trouvé,
yield
il (ou les ajouter dans une liste);
- une fois qu'une liste est entièrement explorée, retournez à la liste parent en utilisant le
stack
retour address
(et index
) .
Notez également que cela équivaut à un DFS dans une arborescence où certains nœuds sont des sous-listes A = [1, 2]
et certains sont des éléments simples: 0, 1, 2, 3, 4
(pour L = [0, [1,2], 3, 4]
). L'arbre ressemble à ceci:
L
|
-------------------
| | | |
0 --A-- 3 4
| |
1 2
La pré-commande de parcours DFS est: L, 0, A, 1, 2, 3, 4. N'oubliez pas que pour implémenter un DFS itératif, vous avez également besoin d'une pile. L'implémentation que j'ai proposée auparavant aboutit à avoir les états suivants (pour le stack
et le flat_list
):
init.: stack=[(L, 0)]
**0**: stack=[(L, 0)], flat_list=[0]
**A**: stack=[(L, 1), (A, 0)], flat_list=[0]
**1**: stack=[(L, 1), (A, 0)], flat_list=[0, 1]
**2**: stack=[(L, 1), (A, 1)], flat_list=[0, 1, 2]
**3**: stack=[(L, 2)], flat_list=[0, 1, 2, 3]
**3**: stack=[(L, 3)], flat_list=[0, 1, 2, 3, 4]
return: stack=[], flat_list=[0, 1, 2, 3, 4]
Dans cet exemple, la taille maximale de la pile est 2, car la liste d'entrée (et donc l'arborescence) a la profondeur 2.
la mise en oeuvre
Pour l'implémentation, en python, vous pouvez simplifier un peu en utilisant des itérateurs au lieu de simples listes. Les références aux (sous) itérateurs seront utilisées pour stocker les adresses de retour des sous-listes (au lieu d'avoir à la fois l'adresse de liste et l'index). Ce n'est pas une grande différence mais je pense que c'est plus lisible (et aussi un peu plus rapide):
def flatten(iterable):
return list(items_from(iterable))
def items_from(iterable):
cursor_stack = [iter(iterable)]
while cursor_stack:
sub_iterable = cursor_stack[-1]
try:
item = next(sub_iterable)
except StopIteration: # post-order
cursor_stack.pop()
continue
if is_list_like(item): # pre-order
cursor_stack.append(iter(item))
elif item is not None:
yield item # in-order
def is_list_like(item):
return isinstance(item, list)
Notez également que dans is_list_like
I have isinstance(item, list)
, qui pourrait être modifié pour gérer plus de types d'entrée, ici, je voulais juste avoir la version la plus simple où (itérable) n'est qu'une liste. Mais vous pouvez aussi le faire:
def is_list_like(item):
try:
iter(item)
return not isinstance(item, str) # strings are not lists (hmm...)
except TypeError:
return False
Cela considère les chaînes comme des «éléments simples» et, par conséquent, flatten_iter([["test", "a"], "b])
sera renvoyé ["test", "a", "b"]
et non ["t", "e", "s", "t", "a", "b"]
. Remarquez que dans ce cas, iter(item)
est appelé deux fois sur chaque élément, supposons que c'est un exercice pour le lecteur de rendre ce nettoyeur.
Tests et remarques sur d'autres implémentations
En fin de compte, n'oubliez pas que vous ne pouvez pas imprimer une liste imbriquée à l'infini en L
utilisant print(L)
car en interne, il utilisera des appels récursifs à __repr__
( RecursionError: maximum recursion depth exceeded while getting the repr of an object
). Pour la même raison, les solutions à l' flatten
implication str
échoueront avec le même message d'erreur.
Si vous devez tester votre solution, vous pouvez utiliser cette fonction pour générer une liste imbriquée simple:
def build_deep_list(depth):
"""Returns a list of the form $l_{depth} = [depth-1, l_{depth-1}]$
with $depth > 1$ and $l_0 = [0]$.
"""
sub_list = [0]
for d in range(1, depth):
sub_list = [d, sub_list]
return sub_list
Ce qui donne: build_deep_list(5)
>>> [4, [3, [2, [1, [0]]]]]
.