Compreensão de lista sem [] em Python

86

Aderir a uma lista:

>>> ''.join([ str(_) for _ in xrange(10) ])
'0123456789'

join deve ter um iterável.

Aparentemente, joino argumento de é [ str(_) for _ in xrange(10) ], e é uma compreensão de lista .

Veja isso:

>>>''.join( str(_) for _ in xrange(10) )
'0123456789'

Agora, joino argumento de é apenas str(_) for _ in xrange(10), não [], mas o resultado é o mesmo.

Por quê? Será str(_) for _ in xrange(10)também produzir uma lista ou um iterable?

Alcott
fonte
1
Eu imagino que joinprovavelmente está escrito em C e, portanto, funciona muito mais rápido do que uma compreensão de lista ... Tempo de teste!
Joel Cornett
Aparentemente, eu li sua pergunta completamente errado. Parece estar devolvendo um gerador para mim ...
Joel Cornett
18
Apenas uma observação: _não tem nenhum significado especial, é um nome de variável regular. Geralmente é usado como um nome descartável, mas não é o caso (você está usando a variável). Eu evitaria usá-lo em um código (pelo menos dessa forma).
rplnt

Respostas:

69
>>>''.join( str(_) for _ in xrange(10) )

Isso é chamado de expressão geradora e é explicado no PEP 289 .

A principal diferença entre expressões geradoras e compreensões de lista é que as primeiras não criam a lista na memória.

Observe que há uma terceira maneira de escrever a expressão:

''.join(map(str, xrange(10)))
NPE
fonte
1
Como eu sei, um gerador pode ser produzido por meio de uma expressão semelhante a uma tupla, como ( str(_) for _ in xrange(10) ). Mas eu estava confuso, porque o ()pode ser omitido em join, o que significa que o código deveria ser como `'' .join ((str (_) for _ in xrange (10))), certo?
Alcott
2
@Alcott Meu entendimento sobre tuplas é que elas são realmente definidas pela lista de expressões separadas por vírgulas e não pelos parênteses; os parênteses existem apenas para agrupar visualmente os valores em uma atribuição ou para realmente agrupar os valores se a tupla estiver indo para alguma outra lista separada por vírgulas, como uma chamada de função. Isso geralmente é demonstrado executando o código como tup = 1, 2, 3; print(tup). Com isso em mente, usar forcomo parte de uma expressão cria o gerador e os parênteses estão lá apenas para distingui-lo de um loop escrito incorretamente.
Eric Ed Lohmar
133

Os outros entrevistados estavam corretos ao responder que você havia descoberto uma expressão geradora (que tem uma notação semelhante às compreensões de lista, mas sem os colchetes ao redor).

Em geral, genexps (como são carinhosamente conhecidos) são mais eficientes em termos de memória e mais rápidas do que as compreensões de listas.

NO ENTANTO, no caso de ''.join(), a compreensão de uma lista é mais rápida e mais eficiente em termos de memória. O motivo é que a junção precisa fazer duas passagens pelos dados, portanto, na verdade, precisa de uma lista real. Se você der um, ele pode começar seu trabalho imediatamente. Se, em vez disso, você fornecer uma genexp, ela não poderá começar a trabalhar até que crie uma nova lista na memória executando a genexp até a exaustão:

~ $ python -m timeit '"".join(str(n) for n in xrange(1000))'
1000 loops, best of 3: 335 usec per loop
~ $ python -m timeit '"".join([str(n) for n in xrange(1000)])'
1000 loops, best of 3: 288 usec per loop

O mesmo resultado é válido ao comparar itertools.imap versus map :

~ $ python -m timeit -s'from itertools import imap' '"".join(imap(str, xrange(1000)))'
1000 loops, best of 3: 220 usec per loop
~ $ python -m timeit '"".join(map(str, xrange(1000)))'
1000 loops, best of 3: 212 usec per loop
Raymond Hettinger
fonte
4
@lazyr Seu segundo tempo está dando muito trabalho. Não envolva um genexp em torno de um listcomp - apenas use um genexp diretamente. Não é à toa que você tem horários estranhos.
Raymond Hettinger
11
Você poderia explicar por que ''.join()precisa de 2 passagens no iterador para construir uma string?
ovgolovin
28
@ovgolovin Eu acho que a primeira passagem é somar os comprimentos das strings de forma a poder alocar a quantidade correta de memória para a string concatenada, enquanto a segunda passagem é copiar as strings individuais no espaço alocado.
Lauritz V. Thaulow
20
@lazyr Essa suposição está correta. Isso é exatamente o que str.join faz :-)
Raymond Hettinger
4
Às vezes eu realmente sinto falta da capacidade de "adicionar como favorito" uma resposta específica no SO.
Air
5

Seu segundo exemplo usa uma expressão geradora em vez de uma compreensão de lista. A diferença é que com a compreensão da lista, uma lista é totalmente construída e passada para .join(). Com a expressão geradora, os itens são gerados um a um e consumidos por .join(). Este último usa menos memória e geralmente é mais rápido.

Por acaso, o construtor de lista consumirá qualquer iterável, incluindo uma expressão geradora. Então:

[str(n) for n in xrange(10)]

é apenas "açúcar sintático" para:

list(str(n) for n in xrange(10))

Em outras palavras, uma compreensão de lista é apenas uma expressão geradora que se transforma em uma lista.

kindall
fonte
2
Tem certeza de que são equivalentes sob o capô? Timeit diz:: [str(x) for x in xrange(1000)]262 usec list(str(x) for x in xrange(1000)),: 304 usec.
Lauritz V. Thaulow
2
@lazyr Você está certo. A compreensão da lista é mais rápida. E esta é a razão pela qual as compreensões de lista vazam no Python 2.x. Isto é o que GVR escreveu: "" Este foi um artefato da implementação original de compreensões de lista; foi um dos "segredinhos sujos" do Python durante anos. Começou como um compromisso intencional para fazer compreensões de listas incrivelmente rápidas e, embora não fosse uma armadilha comum para iniciantes, definitivamente feriu as
ovgolovin
3
@ovgolovin O motivo de listcomp ser mais rápido é porque o join precisa criar uma lista antes de começar a funcionar. O "vazamento" ao qual você se refere não é um problema de velocidade - significa apenas que a variável de indução do loop é exposta fora do listcomp.
Raymond Hettinger
1
@RaymondHettinger Então, o que essa palavra significa "Tudo começou como um compromisso intencional para tornar as compreensões de listas incrivelmente rápidas "? Pelo que entendi, há uma conexão do vazamento com os problemas de velocidade. GVR também escreveu: "Para expressões geradoras não poderíamos fazer isso. Expressões geradoras são implementadas usando geradores, cuja execução requer um quadro de execução separado. Assim, expressões geradoras (especialmente se iterarem em uma sequência curta) foram menos eficientes do que as compreensões de lista . "
ovgolovin
4
@ovgolovin Você deu um salto incorreto de um detalhe de implementação listcomp para o motivo pelo qual str.join tem o desempenho que tem. Uma das primeiras linhas no código str.join é seq = PySequence_Fast(orig, "");e essa é a única razão pela qual os iteradores são executados mais lentamente do que listas ou tuplas ao chamar str.join (). Você é bem-vindo para iniciar um bate-papo se quiser discuti-lo mais (sou o autor do PEP 289, o criador do opcode LIST_APPEND e aquele que otimizou o construtor list (), então eu tenho alguns familiaridade com o assunto).
Raymond Hettinger
5

Como mencionado, é uma expressão geradora .

Da documentação:

Os parênteses podem ser omitidos em chamadas com apenas um argumento. Consulte a seção Solicitações para obter os detalhes.

Monkut
fonte
4

Se estiver entre parênteses, mas não entre colchetes, é tecnicamente uma expressão geradora. Expressões geradoras foram introduzidas pela primeira vez no Python 2.4.

http://wiki.python.org/moin/Generators

A parte após a junção ( str(_) for _ in xrange(10) )é, por si só, uma expressão geradora. Você poderia fazer algo como:

mylist = (str(_) for _ in xrange(10))
''.join(mylist)

e significa exatamente a mesma coisa que você escreveu no segundo caso acima.

Os geradores têm algumas propriedades muito interessantes, e a menos importante delas é que eles não acabam alocando uma lista inteira quando você não precisa de uma. Em vez disso, uma função como join "bombeia" os itens para fora da expressão do gerador, um de cada vez, fazendo seu trabalho nas pequenas partes intermediárias.

Em seus exemplos particulares, lista e gerador provavelmente não funcionam de maneira terrivelmente diferente, mas em geral, eu prefiro usar expressões de gerador (e até funções de gerador) sempre que posso, principalmente porque é extremamente raro um gerador ser mais lento do que uma lista completa materialização.

sblom
fonte
1

Isso é um gerador, em vez de uma compreensão de lista. Os geradores também são iteráveis, mas em vez de criar a lista inteira primeiro e depois passá-la para a junção, ele passa cada valor no xrange um por um, o que pode ser muito mais eficiente.

Daniel Roseman
fonte
0

O argumento para sua segunda joinchamada é uma expressão geradora. Ele produz um iterável.

Michael J. Barber
fonte