¿Cuándo no es un buen momento para usar generadores de Python?

83

Esto es más bien lo inverso de ¿Para qué se pueden usar las funciones del generador de Python? : los generadores de Python, las expresiones del generador y el itertoolsmódulo son algunas de mis características favoritas de Python en estos días. Son especialmente útiles al configurar cadenas de operaciones para realizar en una gran cantidad de datos; a menudo los uso cuando proceso archivos DSV.

Entonces, ¿cuándo no es un buen momento para usar un generador, o una expresión generadora, o una itertoolsfunción?

  • Cuando debería preferir zip()más itertools.izip(), o
  • range()terminado xrange(), o
  • [x for x in foo]terminado (x for x in foo)?

Obviamente, eventualmente necesitamos "resolver" un generador en datos reales, generalmente creando una lista o iterando sobre ella con un ciclo que no sea generador. A veces solo necesitamos saber la longitud. Esto no es lo que estoy preguntando.

Usamos generadores para no asignar nuevas listas a la memoria para datos provisionales. Esto tiene sentido especialmente para grandes conjuntos de datos. ¿Tiene sentido también para pequeños conjuntos de datos? ¿Existe una compensación notable entre memoria y CPU?

Estoy especialmente interesado si alguien ha elaborado algunos perfiles sobre esto, a la luz de la discusión reveladora sobre el rendimiento de comprensión de listas frente a map () y filter () . ( enlace alternativo )

David Eyk
fuente
2
Hice una pregunta similar aquí e hice un análisis para encontrar que en mi ejemplo particular, las listas son más rápidas para iterables de longitud<5 .
Alexander McFarlane
¿Responde esto a tu pregunta? Expresiones generadoras frente a comprensión de
listas

Respuestas:

57

Utilice una lista en lugar de un generador cuando:

1) Debe acceder a los datos varias veces (es decir, almacenar en caché los resultados en lugar de volver a calcularlos):

for i in outer:           # used once, okay to be a generator or return a list
    for j in inner:       # used multiple times, reusing a list is better
         ...

2) Necesita acceso aleatorio (o cualquier acceso que no sea el orden secuencial hacia adelante):

for i in reversed(data): ...     # generators aren't reversible

s[i], s[j] = s[j], s[i]          # generators aren't indexable

3) Debe unir cadenas (lo que requiere dos pasadas sobre los datos):

s = ''.join(data)                # lists are faster than generators in this use case

4) Está utilizando PyPy, que a veces no puede optimizar el código del generador tanto como puede con las llamadas a funciones normales y las manipulaciones de listas.

Raymond Hettinger
fuente
Para el n. ° 3, ¿no podrían evitarse las dos pasadas usando ireducepara replicar la combinación?
Platinum Azure
¡Gracias! No estaba al tanto del comportamiento de unión de cuerdas. ¿Puede proporcionar o vincular a una explicación de por qué requiere dos pases?
David Eyk
5
@DavidEyk str.join hace una pasada para sumar las longitudes de todos los fragmentos de cadena para que sepa mucha memoria para asignar para el resultado final combinado. La segunda pasada copia los fragmentos de cadena en el nuevo búfer para crear una sola cadena nueva. Ver hg.python.org/cpython/file/82fd95c2851b/Objects/stringlib/…
Raymond Hettinger
1
Interesante, uso generadores muy a menudo para unir srings. Pero, me pregunto, ¿cómo funciona si necesita dos pases? por ejemplo''.join('%s' % i for i in xrange(10))
bgusach
4
@ ikaros45 Si la entrada para unirse no es una lista, tiene que hacer un trabajo adicional para construir una lista temporal para las dos pasadas. Aproximadamente este `` datos = datos si es instancia (datos, lista) otra lista (datos); n = suma (mapa (len, datos)); buffer = bytearray (n); ... <copiar fragmentos en el búfer> `` `.
Raymond Hettinger
40

En general, no use un generador cuando necesite operaciones de lista, como len (), reversed (), etc.

También puede haber ocasiones en las que no desee una evaluación perezosa (por ejemplo, hacer todo el cálculo por adelantado para poder liberar un recurso). En ese caso, una expresión de lista podría ser mejor.

Ryan Ginstrom
fuente
25
Además, hacer todo el cálculo por adelantado garantiza que si el cálculo de los elementos de la lista arroja una excepción, se lanzará en el punto donde se crea la lista , no en el bucle que posteriormente la recorre. Si necesita garantizar un procesamiento sin errores de toda la lista antes de continuar, los generadores no son buenos.
Ryan C. Thompson
4
Ese es un buen punto. Es muy frustrante llegar a la mitad del procesamiento de un generador, solo para que todo explote. Potencialmente puede ser peligroso.
David Eyk
26

Perfil, perfil, perfil.

Perfilar su código es la única forma de saber si lo que está haciendo tiene algún efecto.

La mayoría de los usos de xrange, generators, etc. son de tamaño estático, pequeños conjuntos de datos. Es solo cuando se llega a grandes conjuntos de datos que realmente se marca la diferencia. range () vs xrange () es principalmente una cuestión de hacer que el código se vea un poquito más feo, y no perder nada, y tal vez ganar algo.

Perfil, perfil, perfil.

Jerub
fuente
1
Perfil, de hecho. Uno de estos días, intentaré hacer una comparación empírica. Hasta entonces, solo esperaba que alguien más lo hubiera hecho. :)
David Eyk
Perfil, perfil, perfil. Estoy completamente de acuerdo. Perfil, perfil, perfil.
Jeppe
17

Nunca se debe favorecer zipmás izip, rangemás xrangeo listas por comprensión más comprensiones del generador. En Python 3.0 rangetiene xrange-como la semántica y ziptiene izip-como la semántica.

Las list(frob(x) for x in foo)listas por comprensión son en realidad más claras, como en las ocasiones en que necesita una lista real.

Steven Huwig
fuente
3
@Steven No estoy en desacuerdo, pero me pregunto cuál es el razonamiento detrás de tu respuesta. ¿Por qué las comprensiones zip, range y list nunca deberían ser favorecidas sobre la correspondiente versión "lazy"?
mhawke
porque, como dijo, el antiguo comportamiento de zip y range desaparecerá pronto.
@Steven: Buen punto. Me había olvidado de estos cambios en 3.0, lo que probablemente significa que alguien está convencido de su superioridad general. Re: listas por comprensión, a menudo son más claras (¡y más rápidas que los forbucles expandidos !), Pero se pueden escribir fácilmente listas por comprensión incomprensibles.
David Eyk
9
Entiendo lo que quieres decir, pero encuentro la []forma lo suficientemente descriptiva (y más concisa, y menos desordenada, en general). Pero esto es solo una cuestión de gustos.
David Eyk
4
Las operaciones de lista son más rápidas para tamaños de datos pequeños, pero todo es rápido cuando el tamaño de datos es pequeño, por lo que siempre debe preferir los generadores a menos que tenga una razón específica para usar listas (por tales razones, consulte la respuesta de Ryan Ginstrom).
Ryan C. Thompson
7

Como mencionas, "Esto tiene sentido especialmente para grandes conjuntos de datos", creo que esto responde a tu pregunta.

Si no golpea ninguna pared, en cuanto al rendimiento, aún puede atenerse a las listas y funciones estándar. Luego, cuando tenga problemas con el rendimiento, haga el cambio.

Sin embargo, como lo menciona @ u0b34a0f6ae en los comentarios, el uso de generadores al principio puede hacer que sea más fácil escalar a conjuntos de datos más grandes.

monkut
fuente
5
+1 Generators hace que su código esté más listo para grandes conjuntos de datos sin que tenga que anticiparlo.
u0b34a0f6ae
6

En cuanto al rendimiento: si usas psyco, las listas pueden ser un poco más rápidas que los generadores. En el siguiente ejemplo, las listas son casi un 50% más rápidas cuando se usa psyco.full ()

import psyco
import time
import cStringIO

def time_func(func):
    """The amount of time it requires func to run"""
    start = time.clock()
    func()
    return time.clock() - start

def fizzbuzz(num):
    """That algorithm we all know and love"""
    if not num % 3 and not num % 5:
        return "%d fizz buzz" % num
    elif not num % 3:
        return "%d fizz" % num
    elif not num % 5:
        return "%d buzz" % num
    return None

def with_list(num):
    """Try getting fizzbuzz with a list comprehension and range"""
    out = cStringIO.StringIO()
    for fibby in [fizzbuzz(x) for x in range(1, num) if fizzbuzz(x)]:
        print >> out, fibby
    return out.getvalue()

def with_genx(num):
    """Try getting fizzbuzz with generator expression and xrange"""
    out = cStringIO.StringIO()
    for fibby in (fizzbuzz(x) for x in xrange(1, num) if fizzbuzz(x)):
        print >> out, fibby
    return out.getvalue()

def main():
    """
    Test speed of generator expressions versus list comprehensions,
    with and without psyco.
    """

    #our variables
    nums = [10000, 100000]
    funcs = [with_list, with_genx]

    #  try without psyco 1st
    print "without psyco"
    for num in nums:
        print "  number:", num
        for func in funcs:
            print func.__name__, time_func(lambda : func(num)), "seconds"
        print

    #  now with psyco
    print "with psyco"
    psyco.full()
    for num in nums:
        print "  number:", num
        for func in funcs:
            print func.__name__, time_func(lambda : func(num)), "seconds"
        print

if __name__ == "__main__":
    main()

Resultados:

without psyco
  number: 10000
with_list 0.0519102208309 seconds
with_genx 0.0535933367509 seconds

  number: 100000
with_list 0.542204280744 seconds
with_genx 0.557837353115 seconds

with psyco
  number: 10000
with_list 0.0286369007033 seconds
with_genx 0.0513424889137 seconds

  number: 100000
with_list 0.335414877839 seconds
with_genx 0.580363490491 seconds
Ryan Ginstrom
fuente
1
Eso es porque la psicología no acelera los generadores en absoluto, por lo que es más una deficiencia de la psicópata que de los generadores. Sin embargo, buena respuesta.
Steven Huwig
4
Además, el psicópata está prácticamente sin mantenimiento ahora. Todos los desarrolladores están dedicando tiempo al JIT de PyPy que, según mi conocimiento, optimiza los generadores.
Noufal Ibrahim
3

En lo que respecta al rendimiento, no puedo pensar en ningún momento en el que desee utilizar una lista en lugar de un generador.

Jason Baker
fuente
all(True for _ in range(10 ** 8))es más lento que all([True for _ in range(10 ** 8)])en Python 3.8. Preferiría una lista sobre un generador aquí
ggorlen
3

Nunca he encontrado una situación en la que los generadores obstaculicen lo que intentas hacer. Sin embargo, hay muchos casos en los que el uso de generadores no le ayudaría más que no usarlos.

Por ejemplo:

sorted(xrange(5))

No ofrece ninguna mejora sobre:

sorted(range(5))
Jeremy Cantrell
fuente
4
Ninguno de esos ofrece ninguna mejora range(5), ya que la lista resultante ya está ordenada.
dan04
3

Debería preferir listas por comprensión si necesita mantener los valores para otra cosa más adelante y el tamaño de su conjunto no es demasiado grande.

Por ejemplo: está creando una lista que recorrerá varias veces más adelante en su programa.

Hasta cierto punto, puede pensar en los generadores como un reemplazo de la iteración (bucles) frente a las listas por comprensión como un tipo de inicialización de la estructura de datos. Si desea mantener la estructura de datos, utilice listas por comprensión.

menta
fuente
Si solo necesita una vista anticipada / retrospectiva limitada en la transmisión, entonces tal vez itertools.tee()pueda ayudarlo. Pero en general, si desea más de una pasada, o acceso aleatorio a algunos datos intermedios, haga una lista / conjunto / dictado.
Beni Cherniavsky-Paskin