Ao tentar responder a essa pergunta, você realmente precisa fornecer as limitações do código que propõe como solução. Se fosse apenas sobre performances, eu não me importaria muito, mas a maioria dos códigos propostos como solução (incluindo a resposta aceita) falha ao achatar qualquer lista com profundidade maior que 1000.
Quando digo a maioria dos códigos quero dizer todos os códigos que usam qualquer forma de recursão (ou chamam uma função de biblioteca padrão que é recursiva). Todos esses códigos falham porque, para cada chamada recursiva feita, a pilha (chamada) cresce em uma unidade e a pilha de chamadas python (padrão) tem um tamanho de 1000.
Se você não estiver muito familiarizado com a pilha de chamadas, talvez o seguinte ajude (caso contrário, você pode simplesmente rolar para a Implementação ).
Tamanho da pilha de chamadas e programação recursiva (analogia do calabouço)
Encontrar o tesouro e sair
Imagine que você entra em uma enorme masmorra com salas numeradas , procurando um tesouro. Você não conhece o lugar, mas tem algumas indicações sobre como encontrar o tesouro. Cada indicação é um enigma (a dificuldade varia, mas você não pode prever o quão difícil será). Você decide pensar um pouco sobre uma estratégia para economizar tempo, faz duas observações:
- É difícil (longo) encontrar o tesouro, pois você terá que resolver enigmas (potencialmente difíceis) para chegar lá.
- Uma vez que o tesouro encontrado, retornar à entrada pode ser fácil, basta usar o mesmo caminho na outra direção (embora isso precise de um pouco de memória para recuperar o seu caminho).
Ao entrar na masmorra, você nota um pequeno caderno aqui. Você decide usá-lo para anotar todas as salas que sair depois de resolver um enigma (ao entrar em uma nova sala), dessa forma você poderá voltar para a entrada. Essa é uma ideia genial, você nem gasta um centavo implementando sua estratégia.
Você entra na masmorra, resolvendo com grande sucesso os primeiros 1001 enigmas, mas aí vem algo que você não havia planejado, não havia mais espaço no caderno emprestado. Você decide abandonar sua missão, pois prefere não ter o tesouro do que se perder para sempre dentro da masmorra (que parece realmente inteligente).
Executando um programa recursivo
Basicamente, é exatamente a mesma coisa que encontrar o tesouro. A masmorra é a memória do computador , seu objetivo agora não é encontrar um tesouro, mas calcular algumas funções (encontre f (x) para um dado x ). As indicações são simplesmente sub-rotinas que o ajudarão a resolver f (x) . Sua estratégia é a mesma da pilha de chamadas , o notebook é a pilha, as salas são os endereços de retorno das funções:
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))
O problema que você encontrou no calabouço será o mesmo aqui, a pilha de chamadas tem um tamanho finito (aqui 1000) e, portanto, se você digitar muitas funções sem retornar, preencherá a pilha de chamadas e ocorrerá um erro que parecerá como "Caro aventureiro, sinto muito, mas seu notebook está cheio" :RecursionError: maximum recursion depth exceeded
. Observe que você não precisa de recursão para preencher a pilha de chamadas, mas é muito improvável que um programa não recursivo chame 1000 funções sem nunca retornar. É importante entender também que, quando você retorna de uma função, a pilha de chamadas é liberada do endereço usado (daí o nome "pilha", o endereço de retorno é inserido antes de inserir uma função e retirado ao retornar). No caso especial de uma recursão simples (uma funçãof
que se autodenomina repetidamente - você entrará f
repetidamente até que o cálculo esteja concluído (até que o tesouro seja encontrado) e retornará de f
até voltar ao local para o qual você ligou f
. A pilha de chamadas nunca será liberada de nada até o final, onde será liberada de todos os endereços de retorno, um após o outro.
Como evitar esse problema?
Na verdade, isso é bem simples: "não use recursão se você não souber o quão profundo ela pode ser". Isso nem sempre é verdade, pois em alguns casos, a recursão da chamada de cauda pode ser otimizada (TCO) . Mas em python, esse não é o caso, e mesmo a função recursiva "bem escrita" não otimiza o uso da pilha. Há um post interessante de Guido sobre esta questão: Eliminação da recursão da cauda .
Existe uma técnica que você pode usar para tornar iterativa qualquer função recursiva, essa técnica que poderíamos chamar de trazer seu próprio notebook . Por exemplo, em nosso caso específico, simplesmente estamos explorando uma lista, entrar em uma sala é equivalente a entrar em uma sub-lista; a pergunta que você deve fazer é: como posso voltar de uma lista para sua lista de pais? A resposta não é tão complexa, repita o seguinte até que stack
esteja vazio:
- empurre a lista atual
address
e index
em um stack
ao entrar em uma nova sub-lista (note que um endereço de lista + índice é também um endereço, portanto, apenas usar exatamente a mesma técnica usada pela pilha de chamadas);
- toda vez que um item é encontrado,
yield
ele (ou adiciona-o a uma lista);
- quando uma lista for totalmente explorada, volte à lista de pais usando o
stack
retorno address
(e index
) .
Observe também que isso é equivalente a um DFS em uma árvore em que alguns nós são sublistas A = [1, 2]
e outros são itens simples: 0, 1, 2, 3, 4
(para L = [0, [1,2], 3, 4]
). A árvore fica assim:
L
|
-------------------
| | | |
0 --A-- 3 4
| |
1 2
A pré-ordem de travessia do DFS é: L, 0, A, 1, 2, 3, 4. Lembre-se de que, para implementar um DFS iterativo, você também "precisa" de uma pilha. A implementação que propus antes resulta em ter os seguintes estados (para 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]
Neste exemplo, o tamanho máximo da pilha é 2, porque a lista de entrada (e, portanto, a árvore) possui profundidade 2.
Implementação
Para a implementação, em python, você pode simplificar um pouco usando iteradores em vez de listas simples. As referências aos (sub) iteradores serão usadas para armazenar os endereços de retorno das sublistas (em vez de ter o endereço da lista e o índice). Essa não é uma grande diferença, mas acho que é mais legível (e também um pouco mais rápida):
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)
Além disso, observe que em is_list_like
I have isinstance(item, list)
, que poderia ser alterado para lidar com mais tipos de entrada, aqui eu só queria ter a versão mais simples em que (iterável) seja apenas uma lista. Mas você também pode fazer isso:
def is_list_like(item):
try:
iter(item)
return not isinstance(item, str) # strings are not lists (hmm...)
except TypeError:
return False
Isso considera as strings como "itens simples" e, portanto flatten_iter([["test", "a"], "b])
, retornará ["test", "a", "b"]
e não ["t", "e", "s", "t", "a", "b"]
. Observe que, nesse caso, iter(item)
é chamado duas vezes em cada item, vamos fingir que é um exercício para o leitor tornar esse limpador mais limpo.
Testes e observações sobre outras implementações
No final, lembre-se de que você não pode imprimir uma lista infinitamente aninhada L
usando, print(L)
porque internamente ela usará chamadas recursivas para __repr__
( RecursionError: maximum recursion depth exceeded while getting the repr of an object
). Pelo mesmo motivo, as soluções de flatten
envolvimento str
falharão com a mesma mensagem de erro.
Se você precisar testar sua solução, poderá usar esta função para gerar uma lista aninhada simples:
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
O que fornece: build_deep_list(5)
>>> [4, [3, [2, [1, [0]]]]]
.