Por que o Python não possui uma função "achatada" para listas?

39

Erlang e Ruby vêm com funções para achatar matrizes. Parece uma ferramenta tão simples e útil para adicionar a um idioma. Pode-se fazer isso:

>>> mess = [[1, [2]], 3, [[[4, 5]], 6]]
>>> mess.flatten()
[1, 2, 3, 4, 5, 6]

Ou até:

>>> import itertools
>>> mess = [[1, [2]], 3, [[[4, 5]], 6]]
>>> list(itertools.flatten(mess))
[1, 2, 3, 4, 5, 6]

Em vez disso, no Python, é preciso passar pelo trabalho de escrever uma função para achatar matrizes do zero. Isso me parece bobo, achatar matrizes é uma coisa tão comum a se fazer. É como ter que escrever uma função personalizada para concatenar duas matrizes.

Eu pesquisei isso no Google sem frutos, então estou perguntando aqui; existe uma razão específica para uma linguagem madura como o Python 3, que vem com centenas de milhares de baterias incluídas, não fornecer um método simples de achatar matrizes? A ideia de incluir tal função foi discutida e rejeitada em algum momento?

Hubro
fonte
2
@ Detly: Aconteceu que eu perdi o achatamento ultimamente ao usar várias consultas para recuperar dados de diferentes fontes. Cada consulta retorna uma lista de dicionários; portanto, no final, tenho uma lista de listas de dicionários a serem transformados em uma lista de dicionários. Eu usei um loop +, extendmas achatar teria sido muito mais elegante. No entanto, pergunto se esse padrão é comum o suficiente para justificar o achatamento na biblioteca padrão.
Giorgio
4
"Quero dizer, imagine se você introduzir um bug no seu código que, inadvertidamente, altere a estrutura dos seus dados. O flatten ainda funcionará, mas produzirá completamente os resultados errados.": Essa é uma das razões pelas quais eu gosto de linguagens de tipo estaticamente. ;-)
Giorgio
2
@BryanOakley Ver comentário anterior também (embora não para listas de vários níveis, achatando em geral é comum)
Izkata
3
Ele está embutido no Mathemaica e eu o uso extensivamente.
Por Alexandersson

Respostas:

34

As propostas para uma flattenfunção a ser adicionada à biblioteca padrão aparecem periodicamente nas listas de discussão python-dev e python-ideas . Os desenvolvedores de Python geralmente respondem com os seguintes pontos:

  1. Um nivelamento de um nível (transformando um iterável de iterável em um único iterável) é uma expressão trivial de uma linha (x for y in z for x in y)e, em qualquer caso, já está na biblioteca padrão com o nome itertools.chain.from_iterable.

  2. Quais são os casos de uso para um nivelamento de múltiplos níveis de uso geral? São realmente atraentes o suficiente para que a função seja adicionada à biblioteca padrão?

  3. Como um achatador multi-nível de uso geral decidirá quando achatar e quando sair sozinho? Você pode pensar que uma regra como "achatar qualquer coisa que suporte a interface iterável" funcionaria, mas isso levaria a um loop infinito para flatten('a').

Veja, por exemplo, Raymond Hettinger :

Foi discutido ad nauseam em comp.lang.python. As pessoas parecem gostar de escrever suas próprias versões de achatar mais do que encontrar casos de uso legítimos que ainda não têm soluções triviais.

Um nivelador de uso geral precisa de alguma maneira de saber o que é atômico e o que pode ser subdividido. Além disso, não é óbvio como o algoritmo deve ser estendido para abranger entradas com estruturas de dados semelhantes a árvores com dados em nós e folhas (pré-encomenda, pós-encomenda, passagem de encomendas, etc.)

Gareth Rees
fonte
Apenas para ser explícito, isso significa que a flattenfunção de um nível pode ser definida como lambda z: [x for y in z for x in y].
9786 Christopher Martin
1
"Um nivelador de uso geral precisa de alguma maneira de saber o que é atômico e o que pode ser subdividido.": Isso soa como um problema que pode ser resolvido usando OOP: cada objeto pode ter um flattenmétodo. A implementação desse método deve recursivamente chamar flattenseu subcomponente, se o objeto for um composto. Infelizmente, o AFAIK nem todo valor é um objeto no Python. No Ruby, ele deve funcionar.
Giorgio
1
um auxiliar de achatamento para um achatamento de um nível, em vez de um "for in for" contínuo já é um IMO bom o suficiente. facilmente legível
dtc
2
@Giorgio Python evita esses métodos. Os protocolos são preferidos, e acho que eles são muito mais fáceis de trabalhar do que um design de POO, já que muitas vezes você nem precisa implementar muito.
jpmc26
8

Ele vem com esse método, mas não o chama achatado. É chamado de " cadeia ". Ele retorna um iterador no qual você precisará usar a função list () para transformá-lo novamente em uma lista. Se você não quiser usar um *, poderá usar a segunda versão "from_iterator". Funciona da mesma maneira no Python 3. Ele falhará se a entrada da lista não for uma lista de listas.

[[1], [2, 3], [3, 4, 5]] #yes
[1, 2, [5, 6]] #no

Houve uma vez um método flatten no módulo compiler.ast, mas este foi preterido no 2.6 e depois removido no 3.0. A recursão arbitrária em profundidade, necessária para listas aninhadas arbitrariamente, não funciona bem com a profundidade máxima recursiva conservadora do Python. O motivo para a remoção do compilador deveu-se em grande parte ao fato de ser uma bagunça . O compilador foi transformado em ast, mas o achatamento foi deixado para trás.

A profundidade arbitrária pode ser alcançada com os arrays de numpy e o achatamento dessa biblioteca.

Engenheiro Mundial
fonte
A chain.from_iteratorfunção, como você disse, só pode ser usada para achatar listas bidimensionais. Um actualy função achatar, que aceita quaisquer quantidades de listas aninhadas e retorna uma lista unidimensional, ainda seria maciçamente útil em muitos casos (pelo menos na minha opinião)
Hubro
2
@Hubro: "em muitos casos" - você pode citar seis?
Gareth Rees
1
@GarethRees: Dei alguns exemplos aqui: programmers.stackexchange.com/questions/254279/…
Hubro
Eu também chegaria ao ponto de argumentar que, se essa outra linguagem realmente fornecer esse recurso para achatar uma lista da maneira muito simples descrita, esse é um dos argumentos mais convincentes no suporte à adição dessa capacidade simples ao Python.
Bobort 15/09/16
Ele retorna um iterador ou um gerador?
jpmc26
-1

... talvez porque não seja tão difícil escrever você mesmo

def flatten(l): return flatten(l[0]) + (flatten(l[1:]) if len(l) > 1 else []) if type(l) is list else [l]

... e depois achatar tudo o que quiser :)

>>> flatten([1,[2,3],4])
[1, 2, 3, 4]
>>> flatten([1, [2, 3], 4, [5, [6, {'name': 'some_name', 'age':30}, 7]], [8, 9, [10, [11, [12, [13, {'some', 'set'}, 14, [15, 'some_string'], 16], 17, 18], 19], 20], 21, 22, [23, 24], 25], 26, 27, 28, 29, 30])
[1, 2, 3, 4, 5, 6, {'age': 30, 'name': 'some_name'}, 7, 8, 9, 10, 11, 12, 13, set(['set', 'some']), 14, 15, 'some_string', 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30]
>>> 
Shreyas
fonte
8
O asker está ciente disso: "no Python, é preciso passar pelo trabalho de escrever uma função para achatar arrays do zero". Isso nem tenta responder à pergunta: "Isso me parece bobo, achatar matrizes é uma coisa tão comum a fazer. É como ter que escrever uma função personalizada para concatenar duas matrizes".
mosquito
1
Fora de tópico ... Mas super legal :-) !!
SeF
essa resposta é como dizer ao OP que ele não é um bom desenvolvedor, porque ele não sabia como codificar a função. Sugiro que você modifique o início de sua resposta, porque esse é um código útil para quem se depara com a pergunta, mesmo que fora de tópico.
Federico Bonelli