Al intentar responder a una pregunta de este tipo, realmente necesita dar las limitaciones del código que propone como solución. Si solo se tratara de actuaciones, no me importaría demasiado, pero la mayoría de los códigos propuestos como solución (incluida la respuesta aceptada) no logran aplanar ninguna lista que tenga una profundidad superior a 1000.
Cuando digo la mayoría de los códigos, me refiero a todos los códigos que usan cualquier forma de recursión (o llaman a una función de biblioteca estándar que es recursiva). Todos estos códigos fallan porque por cada llamada recursiva realizada, la pila (llamada) crece en una unidad, y la pila de llamadas python (predeterminada) tiene un tamaño de 1000.
Si no está demasiado familiarizado con la pila de llamadas, quizás lo siguiente le ayude (de lo contrario, puede desplazarse a la Implementación ).
Tamaño de la pila de llamadas y programación recursiva (analogía de mazmorra)
Encontrar el tesoro y salir
Imagina que entras en una mazmorra enorme con habitaciones numeradas , buscando un tesoro. No conoce el lugar, pero tiene algunas indicaciones sobre cómo encontrar el tesoro. Cada indicación es un acertijo (la dificultad varía, pero no se puede predecir qué tan difícil será). Decides pensar un poco en una estrategia para ahorrar tiempo, haces dos observaciones:
- Es difícil (largo) encontrar el tesoro ya que tendrás que resolver acertijos (potencialmente difíciles) para llegar allí.
- Una vez que encuentre el tesoro, regresar a la entrada puede ser fácil, solo tiene que usar el mismo camino en la otra dirección (aunque esto necesita un poco de memoria para recordar su camino).
Al entrar en la mazmorra, ves un pequeño cuaderno aquí. Decide usarlo para escribir cada habitación que salga después de resolver un acertijo (al ingresar a una nueva habitación), de esta manera podrá regresar a la entrada. Esa es una idea genial, ni siquiera gastará un centavo implementando su estrategia.
Entras al calabozo, resolviendo con gran éxito los primeros 1001 acertijos, pero aquí viene algo que no habías planeado, no te queda espacio en el cuaderno que tomaste prestado. Decides abandonar tu búsqueda, ya que prefieres no tener el tesoro que perderte para siempre dentro de la mazmorra (eso parece inteligente).
Ejecutando un programa recursivo
Básicamente, es exactamente lo mismo que encontrar el tesoro. El calabozo es la memoria de la computadora , su objetivo ahora no es encontrar un tesoro sino calcular alguna función (encontrar f (x) para una x dada ). Las indicaciones simplemente son subrutinas que lo ayudarán a resolver f (x) . Su estrategia es la misma que la estrategia de la pila de llamadas , el cuaderno es la pila, las salas son las direcciones de retorno de las funciones:
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))
El problema que encontró en la mazmorra será el mismo aquí, la pila de llamadas tiene un tamaño finito (aquí 1000) y, por lo tanto, si ingresa demasiadas funciones sin regresar, completará la pila de llamadas y tendrá un error que se verá como "Querido aventurero, lo siento mucho, pero su cuaderno está lleno" : RecursionError: maximum recursion depth exceeded
. Tenga en cuenta que no necesita recursividad para llenar la pila de llamadas, pero es muy poco probable que un programa no recursivo llame a 1000 funciones sin volver nunca. También es importante comprender que una vez que regresó de una función, la pila de llamadas se libera de la dirección utilizada (de ahí el nombre "pila", la dirección de retorno se inserta antes de ingresar una función y se retira al regresar). En el caso especial de una recursión simple (una funciónf
que se llama a sí mismo una vez, una y otra vez, entrará f
una y otra vez hasta que finalice el cálculo (hasta que se encuentre el tesoro) y regresará f
hasta que regrese al lugar donde llamó f
en primer lugar. La pila de llamadas nunca se liberará de nada hasta el final, donde se liberará de todas las direcciones de retorno, una tras otra.
¿Cómo evitar este problema?
En realidad, eso es bastante simple: "no uses la recursividad si no sabes qué tan profundo puede llegar". Eso no siempre es cierto, ya que en algunos casos, la recursividad de llamadas de cola se puede optimizar (TCO) . Pero en Python, este no es el caso, e incluso la función recursiva "bien escrita" no optimizará el uso de la pila. Hay una publicación interesante de Guido sobre esta pregunta: Eliminación de recursión de cola .
Hay una técnica que puede usar para hacer que cualquier función recursiva sea iterativa, esta técnica podríamos llamar traer su propio cuaderno . Por ejemplo, en nuestro caso particular simplemente estamos explorando una lista, ingresar a una habitación es equivalente a ingresar una sublista, la pregunta que debe hacerse es cómo puedo volver de una lista a su lista principal. La respuesta no es tan compleja, repita lo siguiente hasta que stack
esté vacío:
- empuje la lista actual
address
y index
en un stack
al ingresar una nueva sublista (tenga en cuenta que una dirección de lista + índice también es una dirección, por lo tanto, solo usamos la misma técnica utilizada por la pila de llamadas);
- cada vez que se encuentra un elemento,
yield
(o agréguelo a una lista);
- una vez que se explora por completo una lista, regrese a la lista principal utilizando el
stack
retorno address
(y index
) .
También tenga en cuenta que esto es equivalente a un DFS en un árbol donde algunos nodos son sublistas A = [1, 2]
y algunos son elementos simples: 0, 1, 2, 3, 4
(para L = [0, [1,2], 3, 4]
). El árbol se ve así:
L
|
-------------------
| | | |
0 --A-- 3 4
| |
1 2
El preorden transversal de DFS es: L, 0, A, 1, 2, 3, 4. Recuerde, para implementar un DFS iterativo también "necesita" una pila. La implementación que propuse antes da como resultado tener los siguientes estados (para el stack
y el 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]
En este ejemplo, el tamaño máximo de la pila es 2, porque la lista de entrada (y, por lo tanto, el árbol) tiene profundidad 2.
Implementación
Para la implementación, en python puede simplificar un poco utilizando iteradores en lugar de listas simples. Las referencias a los (sub) iteradores se utilizarán para almacenar las direcciones de retorno de las sublistas (en lugar de tener tanto la dirección de la lista como el índice). Esto no es una gran diferencia, pero creo que es más legible (y también un poco más rápido):
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)
Además, is_list_like
tenga en cuenta que en I have isinstance(item, list)
, que podría cambiarse para manejar más tipos de entrada, aquí solo quería tener la versión más simple donde (iterable) es solo una lista. Pero también podrías hacer eso:
def is_list_like(item):
try:
iter(item)
return not isinstance(item, str) # strings are not lists (hmm...)
except TypeError:
return False
Esto considera las cadenas como "elementos simples" y, por flatten_iter([["test", "a"], "b])
lo tanto , regresará ["test", "a", "b"]
y no ["t", "e", "s", "t", "a", "b"]
. Tenga en cuenta que en ese caso, iter(item)
se llama dos veces en cada elemento, supongamos que es un ejercicio para el lector hacer esto más limpio.
Pruebas y comentarios sobre otras implementaciones
Al final, recuerde que usted no puede imprimir una lista infinitamente anidada L
usando print(L)
, porque internamente se utilizará para llamadas recursivas __repr__
( RecursionError: maximum recursion depth exceeded while getting the repr of an object
). Por la misma razón, las soluciones a la flatten
participación str
fallarán con el mismo mensaje de error.
Si necesita probar su solución, puede usar esta función para generar una lista anidada 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
Lo que da: build_deep_list(5)
>>> [4, [3, [2, [1, [0]]]]]
.