Quando si cerca di rispondere a una domanda del genere, è necessario fornire le limitazioni del codice proposto come soluzione. Se si trattasse solo di spettacoli, non mi dispiacerebbe troppo, ma la maggior parte dei codici proposti come soluzione (compresa la risposta accettata) non riesce ad appiattire qualsiasi elenco che abbia una profondità maggiore di 1000.
Quando dico la maggior parte dei codici intendo tutti i codici che usano qualsiasi forma di ricorsione (o chiamo una funzione di libreria standard ricorsiva). Tutti questi codici falliscono perché per ciascuna delle chiamate ricorsive effettuate, lo stack (call) cresce di un'unità e lo stack (predefinito) di chiamate python ha una dimensione di 1000.
Se non hai troppa familiarità con lo stack di chiamate, forse ti sarà utile quanto segue (altrimenti puoi semplicemente scorrere fino all'implementazione ).
Dimensione dello stack di chiamata e programmazione ricorsiva (analogia del dungeon)
Trovare il tesoro ed uscire
Immagina di entrare in un enorme sotterraneo con stanze numerate , alla ricerca di un tesoro. Non conosci il posto ma hai alcune indicazioni su come trovare il tesoro. Ogni indicazione è un indovinello (la difficoltà varia, ma non è possibile prevedere quanto saranno difficili). Decidi di pensare un po 'a una strategia per risparmiare tempo, fai due osservazioni:
- È difficile (lungo) trovare il tesoro in quanto dovrai risolvere enigmi (potenzialmente difficili) per arrivarci.
- Una volta trovato il tesoro, tornare all'ingresso potrebbe essere facile, devi solo usare lo stesso percorso nell'altra direzione (anche se questo ha bisogno di un po 'di memoria per ricordare il tuo percorso).
Quando entri nel sotterraneo, noti un piccolo quaderno qui. Decidi di usarlo per annotare ogni stanza che esci dopo aver risolto un indovinello (quando entri in una nuova stanza), in questo modo sarai in grado di tornare indietro all'ingresso. È un'idea geniale, non dovrai nemmeno spendere un centesimo per attuare la tua strategia.
Entri nel sotterraneo, risolvendo con grande successo i primi 1001 enigmi, ma ecco che arriva qualcosa che non avevi pianificato, non hai più spazio nel quaderno che hai preso in prestito. Decidi di abbandonare la tua ricerca poiché preferisci non avere il tesoro piuttosto che perderti per sempre nel sotterraneo (che sembra davvero intelligente).
Esecuzione di un programma ricorsivo
Fondamentalmente, è esattamente la stessa cosa di trovare il tesoro. Il dungeon è la memoria del computer , il tuo obiettivo ora non è trovare un tesoro ma calcolare alcune funzioni (trova f (x) per una data x ). Le indicazioni sono semplicemente delle routine secondarie che ti aiuteranno a risolvere f (x) . La tua strategia è la stessa della strategia di stack di chiamate , il notebook è lo stack, le stanze sono gli indirizzi di ritorno delle funzioni:
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))
Il problema che hai riscontrato nel dungeon sarà lo stesso qui, lo stack di chiamate ha una dimensione finita (qui 1000) e quindi, se inserisci troppe funzioni senza tornare indietro, riempirai lo stack di chiamate e avrai un errore che sembra come "Caro avventuriero, mi dispiace molto ma il tuo quaderno è pieno" :RecursionError: maximum recursion depth exceeded
. Nota che non hai bisogno di ricorsione per riempire lo stack di chiamate, ma è molto improbabile che un programma non ricorsivo chiami 1000 funzioni senza mai tornare. È anche importante capire che una volta tornato da una funzione, lo stack di chiamate viene liberato dall'indirizzo utilizzato (da cui il nome "stack", l'indirizzo di ritorno viene inserito prima di inserire una funzione e estratto al ritorno). Nel caso speciale di una semplice ricorsione (una funzionef
che si chiama una volta - ancora e ancora -) entrerai f
più e più volte fino a quando il calcolo non sarà terminato (fino a quando non sarà trovato il tesoro) e tornerai da f
quando torni al luogo in cui hai chiamato f
in primo luogo. Lo stack di chiamate non verrà mai liberato da nulla fino alla fine in cui verrà liberato uno dopo l'altro da tutti gli indirizzi di ritorno.
Come evitare questo problema?
In realtà è piuttosto semplice: "non usare la ricorsione se non sai quanto in profondità può andare". Questo non è sempre vero come in alcuni casi, la ricorsione di Tail Call può essere ottimizzata (TCO) . Ma in Python non è così, e anche la funzione ricorsiva "ben scritta" non ottimizzerà l'uso dello stack. C'è un post interessante di Guido su questa domanda: Eliminazione della ricorsione della coda .
Esiste una tecnica che puoi usare per rendere iterativa qualsiasi funzione ricorsiva, questa tecnica che potremmo chiamare portare il tuo taccuino . Ad esempio, nel nostro caso particolare stiamo semplicemente esplorando un elenco, entrare in una stanza equivale a inserire un elenco secondario, la domanda che dovresti porti è come posso tornare da un elenco al suo elenco padre? La risposta non è così complessa, ripetere quanto segue fino a quando non stack
è vuota:
- spingere l'elenco corrente
address
e index
in a stack
quando si inserisce un nuovo elenco secondario (si noti che un indirizzo elenco + indice è anche un indirizzo, quindi utilizziamo esattamente la stessa tecnica utilizzata dallo stack di chiamate);
- ogni volta che viene trovato un elemento,
yield
(o aggiungerlo in un elenco);
- una volta che un elenco è stato completamente esplorato, tornare all'elenco parent usando il
stack
tasto return address
(e index
) .
Si noti inoltre che ciò equivale a un DFS in un albero in cui alcuni nodi sono elenchi secondari A = [1, 2]
e altri elementi semplici: 0, 1, 2, 3, 4
(per L = [0, [1,2], 3, 4]
). L'albero si presenta così:
L
|
-------------------
| | | |
0 --A-- 3 4
| |
1 2
Il preordine di attraversamento DFS è: L, 0, A, 1, 2, 3, 4. Ricorda che per implementare un DFS iterativo devi anche "avere bisogno" di uno stack. L'implementazione che ho proposto prima porta ad avere i seguenti stati (per stack
e 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]
In questo esempio, la dimensione massima dello stack è 2, poiché l'elenco di input (e quindi l'albero) ha profondità 2.
Implementazione
Per l'implementazione, in Python puoi semplificare un po 'usando iteratori invece di semplici elenchi. I riferimenti ai (sotto) iteratori verranno utilizzati per memorizzare gli indirizzi di ritorno delle liste secondarie (invece di avere sia l'indirizzo della lista che l'indice). Questa non è una grande differenza, ma penso che sia più leggibile (e anche un po 'più veloce):
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)
Inoltre, nota che in is_list_like
I have isinstance(item, list)
, che potrebbe essere modificato per gestire più tipi di input, qui volevo solo avere la versione più semplice in cui (iterabile) è solo un elenco. Ma potresti anche farlo:
def is_list_like(item):
try:
iter(item)
return not isinstance(item, str) # strings are not lists (hmm...)
except TypeError:
return False
Questo considera le stringhe come "elementi semplici" e quindi flatten_iter([["test", "a"], "b])
ritornerà ["test", "a", "b"]
e non ["t", "e", "s", "t", "a", "b"]
. Si noti che in quel caso, iter(item)
viene chiamato due volte su ogni elemento, facciamo finta che sia un esercizio per il lettore rendere questo più pulito.
Test e osservazioni su altre implementazioni
Alla fine, ricorda che non puoi stampare un elenco infinitamente nidificato L
usando print(L)
perché internamente utilizzerà le chiamate ricorsive a __repr__
( RecursionError: maximum recursion depth exceeded while getting the repr of an object
). Per lo stesso motivo, le soluzioni al flatten
coinvolgimento str
falliranno con lo stesso messaggio di errore.
Se è necessario testare la soluzione, è possibile utilizzare questa funzione per generare un semplice elenco nidificato:
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
Che dà: build_deep_list(5)
>>> [4, [3, [2, [1, [0]]]]]
.