Expresiones de generador versus comprensión de lista

412

¿Cuándo debería usar expresiones generadoras y cuándo debería usar las comprensiones de listas en Python?

# Generator expression
(x*2 for x in range(256))

# List comprehension
[x*2 for x in range(256)]
Solo lectura
fuente
28
podría [exp for x in iter]ser solo azúcar list((exp for x in iter))? o hay una diferencia de ejecución?
b0fh
1
cree que tenía una pregunta relevante, por lo tanto, al usar el rendimiento, ¿podemos usar solo la expresión del generador de una función o tenemos que usar el rendimiento de una función para devolver el objeto generador?
28
@ b0fh Respuesta muy tardía a su comentario: en Python2 hay una pequeña diferencia, la variable de bucle se filtrará de una comprensión de la lista, mientras que una expresión de generador no se filtrará. Compare X = [x**2 for x in range(5)]; print xcon Y = list(y**2 for y in range(5)); print y, el segundo dará un error. En Python3, una comprensión de la lista es, de hecho, el azúcar sintáctico para una expresión generadora alimentada list()como esperaba, por lo que la variable del bucle ya no se filtrará .
Bas Swinckels
13
Sugeriría leer PEP 0289 . Resumido por "Este PEP introduce expresiones generadoras como una generalización de alto rendimiento y memoria eficiente de comprensiones de listas y generadores" . También tiene ejemplos útiles de cuándo usarlos.
icc97
55
@ icc97 También llego ocho años tarde a la fiesta, y el enlace PEP fue perfecto. ¡Gracias por hacer que sea fácil de encontrar!
eenblam

Respuestas:

283

La respuesta de John es buena (esa comprensión de la lista es mejor cuando quieres iterar sobre algo varias veces). Sin embargo, también vale la pena señalar que debe usar una lista si desea usar cualquiera de los métodos de la lista. Por ejemplo, el siguiente código no funcionará:

def gen():
    return (something for something in get_some_stuff())

print gen()[:2]     # generators don't support indexing or slicing
print [5,6] + gen() # generators can't be added to lists

Básicamente, use una expresión generadora si todo lo que está haciendo es iterar una vez. Si desea almacenar y usar los resultados generados, entonces probablemente sea mejor con una comprensión de la lista.

Dado que el rendimiento es la razón más común para elegir uno sobre el otro, mi consejo es que no se preocupe por eso y simplemente elija uno; si encuentra que su programa se está ejecutando demasiado lento, entonces y solo entonces debería regresar y preocuparse por ajustar su código.

Eli Courtwright
fuente
70
A veces tiene que usar generadores, por ejemplo, si está escribiendo corutinas con programación cooperativa con rendimiento. Pero si está haciendo eso, probablemente no esté haciendo esta pregunta;)
Ephemient
12
Sé que esto es antiguo, pero creo que vale la pena señalar que los generadores (y cualquier iterable) se pueden agregar a las listas con extensión: a = [1, 2, 3] b = [4, 5, 6] a.extend(b)- a ahora será [1, 2, 3, 4, 5, 6]. (¿Puedes agregar nuevas líneas en los comentarios?)
jarvisteve
12
@jarvisteve tu ejemplo desmiente las palabras que dices. También hay un buen punto aquí. Las listas se pueden ampliar con generadores, pero entonces no tenía sentido convertirlo en un generador. Los generadores no pueden ampliarse con listas, y los generadores no son del todo iterables. a = (x for x in range(0,10)), b = [1,2,3]por ejemplo. a.extend(b)lanza una excepción. b.extend(a)evaluará todo a, en cuyo caso no tiene sentido convertirlo en un generador en primer lugar.
Slater Victoroff
44
@SlaterTyranus estás 100% correcto, y te voté por precisión. sin embargo, creo que su comentario es una no respuesta útil a la pregunta del OP porque ayudará a aquellos que se encuentran aquí porque escribieron algo como 'combinar generador con comprensión de lista' en un motor de búsqueda.
rbp
1
¿La razón para usar un generador para iterar una vez (por ejemplo, mi preocupación por la falta de memoria anula mi preocupación por "buscar" valores uno por uno ) probablemente todavía se aplica al iterar varias veces? Diría que podría hacer una lista más útil, pero si eso es suficiente para superar las preocupaciones de memoria es otra cosa.
Rob Grant
181

Iterar sobre la expresión del generador o la comprensión de la lista hará lo mismo. Sin embargo, la comprensión de la lista creará la lista completa en la memoria primero, mientras que la expresión del generador creará los elementos sobre la marcha, por lo que puede usarla para secuencias muy grandes (¡y también infinitas!).

dF.
fuente
39
+1 para infinito. No puede hacer eso con una lista, independientemente de lo poco que le importe el rendimiento.
Paul Draper
¿Puedes crear generadores infinitos usando el método de comprensión?
AnnanFay
55
@Annan Solo si ya tienes acceso a otro generador infinito. Por ejemplo, itertools.count(n)es una secuencia infinita de enteros, comenzando desde n, por (2 ** item for item in itertools.count(n))lo que sería una secuencia infinita de los poderes de 2comenzar en 2 ** n.
Kevin
2
Un generador elimina elementos de la memoria después de su iteración. Por lo tanto, es rápido si tiene grandes datos que solo desea mostrar, por ejemplo. No es un cerdo de memoria. Con los generadores, los artículos se procesan 'según sea necesario'. si desea aferrarse a la lista o repetirla nuevamente (para almacenar los elementos), utilice la comprensión de la lista.
j2emanue
102

Utilice las comprensiones de listas cuando el resultado deba repetirse varias veces o cuando la velocidad sea primordial. Use expresiones generadoras donde el rango sea grande o infinito.

Consulte Expresiones de generador y comprensiones de listas para obtener más información.

John Millikin
fuente
2
Esto probablemente será un poco fuera de tema, pero desafortunadamente "no se puede buscar en Google" ... ¿Qué significaría "primordial" en este contexto? No soy un hablante nativo de inglés ... :)
Guillermo Ares
66
@GuillermoAres este es el resultado directo de "googlear" para significar lo más importante
Sнаđошƒаӽ
1
Entonces lists, ¿ son más rápidos que las generatorexpresiones? Al leer la respuesta de dF, me di cuenta de que era al revés.
Hassan Baig
1
Probablemente sea mejor decir que las comprensiones de listas son más rápidas cuando el rango es pequeño, pero a medida que la escala aumenta, se vuelve más valioso calcular los valores sobre la marcha, justo a tiempo para su uso. Eso es lo que hace una expresión generadora.
Kyle
59

El punto importante es que la comprensión de la lista crea una nueva lista. El generador crea un objeto iterable que "filtrará" el material de origen sobre la marcha a medida que consume los bits.

Imagine que tiene un archivo de registro de 2TB llamado "hugefile.txt", y desea el contenido y la longitud de todas las líneas que comienzan con la palabra "ENTRY".

Así que intenta comenzar escribiendo una lista de comprensión:

logfile = open("hugefile.txt","r")
entry_lines = [(line,len(line)) for line in logfile if line.startswith("ENTRY")]

Esto absorbe todo el archivo, procesa cada línea y almacena las líneas coincidentes en su matriz. Por lo tanto, esta matriz podría contener hasta 2 TB de contenido. Eso es mucha RAM, y probablemente no sea práctico para sus propósitos.

Por lo tanto, podemos usar un generador para aplicar un "filtro" a nuestro contenido. En realidad, no se leen datos hasta que comenzamos a iterar sobre el resultado.

logfile = open("hugefile.txt","r")
entry_lines = ((line,len(line)) for line in logfile if line.startswith("ENTRY"))

Ni siquiera se ha leído una sola línea de nuestro archivo todavía. De hecho, digamos que queremos filtrar nuestro resultado aún más:

long_entries = ((line,length) for (line,length) in entry_lines if length > 80)

Todavía no se ha leído nada, pero ahora hemos especificado dos generadores que actuarán sobre nuestros datos como lo deseamos.

Vamos a escribir nuestras líneas filtradas a otro archivo:

outfile = open("filtered.txt","a")
for entry,length in long_entries:
    outfile.write(entry)

Ahora leemos el archivo de entrada. A medida que nuestro forbucle continúa solicitando líneas adicionales, el long_entriesgenerador exige líneas del entry_linesgenerador, devolviendo solo aquellas cuya longitud es mayor a 80 caracteres. Y a su vez, el entry_linesgenerador solicita líneas (filtradas como se indica) del logfileiterador, que a su vez lee el archivo.

Entonces, en lugar de "enviar" datos a su función de salida en forma de una lista completamente poblada, le está dando a la función de salida una forma de "extraer" datos solo cuando sea necesario. Esto es en nuestro caso mucho más eficiente, pero no tan flexible. Los generadores son unidireccionales, un paso; los datos del archivo de registro que hemos leído se descartan de inmediato, por lo que no podemos volver a una línea anterior. Por otro lado, no tenemos que preocuparnos por mantener los datos una vez que hayamos terminado con ellos.

tylerl
fuente
46

El beneficio de una expresión generadora es que usa menos memoria ya que no construye la lista completa a la vez. Las expresiones generadoras se usan mejor cuando la lista es un intermediario, como sumar los resultados o crear un dict a partir de los resultados.

Por ejemplo:

sum(x*2 for x in xrange(256))

dict( (k, some_func(k)) for k in some_list_of_keys )

La ventaja es que la lista no se genera por completo y, por lo tanto, se usa poca memoria (y también debería ser más rápida)

Sin embargo, debe utilizar la comprensión de la lista cuando el producto final deseado es una lista. No va a guardar ninguna memoria utilizando expresiones generadoras, ya que desea la lista generada. También obtiene el beneficio de poder utilizar cualquiera de las funciones de la lista, como ordenado o invertido.

Por ejemplo:

reversed( [x*2 for x in xrange(256)] )
Arrojar
fuente
99
Hay una pista para usted en el lenguaje de que las expresiones generadoras deben usarse de esa manera. Perder los corchetes! sum(x*2 for x in xrange(256))
u0b34a0f6ae
8
sortedy reversedfunciona bien en cualquier iterador, expresiones generadoras incluidas.
marr75
1
Si puede usar 2.7 y superior, ese ejemplo dict () se vería mejor como una comprensión dict (la PEP para eso es más antigua que las expresiones generadoras PEP, pero tardó más en aterrizar)
Jürgen A. Erhard
14

Al crear un generador a partir de un objeto mutable (como una lista), tenga en cuenta que el generador será evaluado en el estado de la lista al momento de usar el generador, no en el momento de la creación del generador:

>>> mylist = ["a", "b", "c"]
>>> gen = (elem + "1" for elem in mylist)
>>> mylist.clear()
>>> for x in gen: print (x)
# nothing

Si hay alguna posibilidad de que su lista se modifique (o un objeto mutable dentro de esa lista) pero necesita el estado en la creación del generador, necesita usar una comprensión de la lista.

freaker
fuente
1
Y esta debería ser la respuesta aceptada. Si sus datos son más grandes que la memoria disponible, siempre debe usar generadores, aunque recorrer la lista en la memoria puede ser más rápido (pero no tiene suficiente memoria para hacerlo).
Marek Marczak
4

A veces puede salirse con la función tee de itertools , devuelve múltiples iteradores para el mismo generador que se pueden usar de forma independiente.

Jacob Rigby
fuente
4

Estoy usando el módulo Hadoop Mincemeat . Creo que este es un gran ejemplo para tomar nota de:

import mincemeat

def mapfn(k,v):
    for w in v:
        yield 'sum',w
        #yield 'count',1


def reducefn(k,v): 
    r1=sum(v)
    r2=len(v)
    print r2
    m=r1/r2
    std=0
    for i in range(r2):
       std+=pow(abs(v[i]-m),2)  
    res=pow((std/r2),0.5)
    return r1,r2,res

Aquí, el generador obtiene números de un archivo de texto (de hasta 15 GB) y aplica cálculos matemáticos simples a esos números usando el mapa de reducción de Hadoop. Si no hubiera utilizado la función de rendimiento, sino una comprensión de la lista, me habría llevado mucho más tiempo calcular las sumas y el promedio (sin mencionar la complejidad del espacio).

Hadoop es un gran ejemplo para utilizar todas las ventajas de los generadores.

Murphy
fuente