Lista de comprensión sin [] en Python

85

Unirse a una lista:

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

join debe tomar un iterable.

Aparentemente, joinel argumento es [ str(_) for _ in xrange(10) ], y es una lista de comprensión .

Mira esto:

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

Ahora, joinel argumento es simplemente str(_) for _ in xrange(10)no [], pero el resultado es el mismo.

¿Por qué? ¿ str(_) for _ in xrange(10)También produce una lista o un iterable?

Alcott
fuente
1
Me imagino que joinprobablemente esté escrito en C y, por lo tanto, se ejecute mucho más rápido que la comprensión de una lista ... ¡Tiempo de prueba!
Joel Cornett
Aparentemente, leí tu pregunta completamente mal. Parece estar devolviendo un generador para mí ...
Joel Cornett
18
Solo una nota: _no tiene un significado especial, es un nombre de variable regular. A menudo se usa como un nombre desechable, pero este no es el caso (está usando la variable). Evitaría usarlo en un código (al menos de esta manera).
rplnt

Respuestas:

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

Esto se denomina expresión generadora y se explica en PEP 289 .

La principal diferencia entre las expresiones generadoras y las listas por comprensión es que las primeras no crean la lista en la memoria.

Tenga en cuenta que hay una tercera forma de escribir la expresión:

''.join(map(str, xrange(10)))
NPE
fuente
1
Como lo sé, un generador se puede producir a través de una expresión similar a una tupla, como ( str(_) for _ in xrange(10) ). Pero estaba confundido, ¿por qué ()se puede omitir join, lo que significa que el código debería ser como `` '' .join ((str (_) para _ en xrange (10))), verdad?
Alcott
1
@Alcott Mi comprensión de las tuplas es que en realidad están definidas por la lista de expresiones separadas por comas y no por el paréntesis; los paréntesis solo están ahí para agrupar visualmente los valores en una asignación o para agrupar realmente los valores si la tupla entrara en alguna otra lista separada por comas, como una llamada a función. Esto a menudo se demuestra ejecutando código como tup = 1, 2, 3; print(tup). Con eso en mente, el uso forcomo parte de una expresión crea el generador y los paréntesis están ahí para distinguirlo de un bucle escrito incorrectamente.
Eric Ed Lohmar
132

Los otros encuestados tenían razón al responder que había descubierto una expresión generadora (que tiene una notación similar a las listas por comprensión, pero sin los corchetes que la rodean).

En general, los genexps (como se les conoce cariñosamente) son más eficientes en memoria y más rápidos que las listas por comprensión.

SIN EMBARGO, en el caso de ''.join(), la comprensión de una lista es más rápida y más eficiente en la memoria. La razón es que join necesita realizar dos pasadas sobre los datos, por lo que en realidad necesita una lista real. Si le da uno, puede comenzar a trabajar de inmediato. Si le da un genexp en su lugar, no puede comenzar a funcionar hasta que construya una nueva lista en la memoria ejecutando genexp hasta el agotamiento:

~ $ 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

El mismo resultado es válido al 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
fuente
4
@lazyr Tu segundo momento está haciendo demasiado trabajo. No envuelva un genexp alrededor de un listcomp, simplemente use un genexp directamente. No es de extrañar que tengas tiempos extraños.
Raymond Hettinger
11
¿Podría explicar por qué ''.join()necesita 2 pases sobre el iterador para construir una cadena?
ovgolovin
27
@ovgolovin Supongo que el primer paso es sumar las longitudes de las cadenas para poder asignar la cantidad correcta de memoria para la cadena concatenada, mientras que el segundo paso es copiar las cadenas individuales en el espacio asignado.
Lauritz V. Thaulow
20
@lazyr Esa suposición es correcta. Eso es exactamente lo que hace str.join :-)
Raymond Hettinger
4
A veces realmente extraño la capacidad de "marcar como favorito" una respuesta específica en SO.
aire
5

Su segundo ejemplo usa una expresión generadora en lugar de una lista de comprensión. La diferencia es que con la comprensión de la lista, una lista se construye y se pasa por completo .join(). Con la expresión del generador, los elementos se generan uno por uno y son consumidos por .join(). Este último usa menos memoria y generalmente es más rápido.

Da la casualidad de que el constructor de listas consumirá felizmente cualquier iterable, incluida una expresión generadora. Entonces:

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

es simplemente "azúcar sintáctico" para:

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

En otras palabras, una lista de comprensión es solo una expresión generadora que se convierte en una lista.

un poco
fuente
2
¿Estás seguro de que son equivalentes debajo del capó? Timeit dice:: [str(x) for x in xrange(1000)]262 usec ,: list(str(x) for x in xrange(1000))304 usec.
Lauritz V. Thaulow
2
@lazyr Tienes razón. La comprensión de listas es más rápida. Y esta es la razón por la que las listas por comprensión se filtran en Python 2.x. Esto es lo que escribió GVR: "" Esto fue un artefacto de la implementación original de las listas por comprensión; fue uno de los "pequeños secretos sucios" de Python durante años. Comenzó como un compromiso intencional para hacer que la comprensión de las listas fuera deslumbrantemente rápida, y aunque no era una trampa común para los principiantes, definitivamente picaba a la gente de vez en cuando ". Python-history.blogspot.com/2010/06/…
ovgolovin
3
@ovgolovin La razón por la que listcomp es más rápido es porque join tiene que crear una lista antes de que pueda comenzar a funcionar. La "fuga" a la que se refiere no es un problema de velocidad, solo significa que la variable de inducción de bucle está expuesta fuera de listcomp.
Raymond Hettinger
1
@RaymondHettinger Entonces, ¿qué significan estas palabras "Comenzó como un compromiso intencional para hacer que las listas de comprensión fueran increíblemente rápidas "? Según entendí, existe una conexión de su fuga con los problemas de velocidad. GVR también escribió: "Para las expresiones generadoras no pudimos hacer esto. Las expresiones generadoras se implementan usando generadores, cuya ejecución requiere un marco de ejecución separado. Por lo tanto, las expresiones generadoras (especialmente si iteran sobre una secuencia corta) eran menos eficientes que las listas por comprensión . "
ovgolovin
4
@ovgolovin Has dado un salto incorrecto de un detalle de implementación de listcomp a por qué str.join funciona de la manera en que lo hace. Una de las primeras líneas en el código str.join es seq = PySequence_Fast(orig, "");y esa es la única razón por la que los iteradores se ejecutan más lentamente que las listas o tuplas al llamar a str.join (). Puede iniciar un chat si desea discutirlo más a fondo (soy el autor de PEP 289, el creador del código de operación LIST_APPEND y el que optimizó el constructor list (), así que tengo algunos familiaridad con el tema).
Raymond Hettinger
5

Como se mencionó, es una expresión generadora .

De la documentación:

Los paréntesis se pueden omitir en llamadas con un solo argumento. Consulte la sección Llamadas para obtener más detalles.

monkut
fuente
4

Si está entre paréntesis, pero no entre paréntesis, técnicamente es una expresión generadora. Las expresiones generadoras se introdujeron por primera vez en Python 2.4.

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

La parte que ( str(_) for _ in xrange(10) )sigue a la combinación es, por sí misma, una expresión generadora. Podrías hacer algo como:

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

y significa exactamente lo mismo que escribió en el segundo caso anterior.

Los generadores tienen algunas propiedades muy interesantes, una de las cuales es que no terminan asignando una lista completa cuando no la necesitas. En cambio, una función como join "bombea" los elementos de la expresión del generador de uno en uno, haciendo su trabajo en las pequeñas partes intermedias.

En sus ejemplos particulares, la lista y el generador probablemente no funcionen de manera muy diferente, pero en general, prefiero usar expresiones de generador (e incluso funciones de generador) siempre que pueda, principalmente porque es extremadamente raro que un generador sea más lento que una lista completa. materialización.

sblom
fuente
1

Eso es un generador, en lugar de una lista de comprensión. Los generadores también son iterables, pero en lugar de crear primero la lista completa y luego pasarla para unirse, pasa cada valor en el rango x uno por uno, lo que puede ser mucho más eficiente.

Daniel Roseman
fuente
0

El argumento de su segunda joinllamada es una expresión generadora. Produce un iterable.

Michael J. Barber
fuente