Menu

Generadores en Python: yield, iteración perezosa y expresiones generadoras

Cómo producen valores perezosamente los generadores en Python — la palabra clave yield, las expresiones generadoras y cuándo ganan a una lista plana.

Una función que pausa

Un generador se parece a una función normal, pero en vez de computar un resultado completo y devolverlo, produce (yields) un valor a la vez, pausando entre producciones hasta que quien está preguntando quiera el siguiente valor.

El más simple posible:

main.py
Output
Click Run to see the output here.

Fíjate en yield en vez de return. La primera vez que for pide un valor, Python ejecuta el cuerpo de la función hasta que golpea yield 1. La función pausa justo ahí, devuelve 1 al bucle, y recuerda exactamente dónde se detuvo — variables y todo. La siguiente iteración retoma donde lo dejó: current += 1, de vuelta al while, yield 2. Y así hasta que la condición del bucle falla, momento en el que el generador simplemente para.

Esa pausa y reanudación es todo el truco.

¿Por qué no construir una lista sin más?

Porque la versión con lista reserva todos los valores por adelantado:

main.py
Output
Click Run to see the output here.

Bien para 5 elementos. Ahora imagina que quieres 50 millones de enteros, y solo te importa el primero que coincida con alguna condición. La versión con lista reserva 50 millones de ints y luego tiras la mayoría. La versión con generador crea exactamente tantos como consuma quien llama. Cuando el bucle for encuentra lo que quiere y hace break, el generador simplemente se detiene.

Ese es el patrón que vale la pena interiorizar: los generadores te dejan escribir código de iteración sin decidir por adelantado cuánto del resultado necesitarás.

Expresiones generadoras

Si has escrito una list comprehension, ya conoces la sintaxis — cambia los corchetes por paréntesis:

main.py
Output
Click Run to see the output here.

squares_gen todavía no computa nada. Es solo una receta. Iterarlo ejecuta la receta un paso a la vez.

Las expresiones generadoras son perfectas como argumentos para funciones que consumen un iterable:

main.py
Output
Click Run to see the output here.

Sin lista intermedia. sum, max y any leen valores de uno en uno, que es exactamente lo que quieren.

Leer un archivo grande, línea a línea

Este es el caso canónico del mundo real para generadores — procesar un archivo demasiado grande para cargarlo en memoria:

def parse_log_lines(path):
    with open(path) as f:
        for line in f:
            if line.startswith("ERROR"):
                yield line.rstrip()

for error in parse_log_lines("app.log"):
    print(error)

El archivo se lee perezosamente. Cada llamada al generador saca una línea del disco, la filtra y la produce. El uso de memoria se mantiene plano independientemente del tamaño del archivo.

Una vez y fuera

Un generador tiene un único pase. Después de haberlo iterado hasta el final, está agotado:

main.py
Output
Click Run to see the output here.

El segundo bucle no imprime nada. El generador no tiene nada más.

Si necesitas iterar más de una vez, o llamas a la función generadora de nuevo para un generador fresco, o materializa la secuencia con list(...) e itera la lista repetidamente. Elige según el coste: reconstruir está bien si el trabajo es barato; una lista está bien si la secuencia es pequeña.

next() e iteración manual

No tienes que usar un bucle for. next() saca un valor a la vez:

main.py
Output
Click Run to see the output here.

StopIteration es cómo un generador señala "he terminado". Los bucles for la atrapan silenciosamente. En código manual puedes pasar un default a next(gen, default) para evitar la excepción.

Generadores infinitos

Como los valores se producen bajo demanda, un generador puede representar una secuencia sin final — mientras el consumidor deje de pedir:

main.py
Output
Click Run to see the output here.

while True con un yield dentro no cuelga el programa — solo significa "si alguien sigue pidiendo, sigue produciendo". El consumidor decide cuándo parar.

Este patrón aparece en datos en streaming, event loops y en cualquier sitio donde saques valores de una fuente que no tiene una longitud definida.

yield from: delegar a otro iterable

Si tu generador quiere producir cada valor de otro iterable, yield from lo hace en una línea:

main.py
Output
Click Run to see the output here.

Sin yield from escribirías un bucle for anidado con yield x dentro. También reenvía correctamente llamadas a send() y throw() si alguna vez las usas — pero para código diario, piénsalo como "produce cada valor de esta cosa".

Cuándo recurrir a un generador

Tres señales de que un generador es la herramienta correcta:

  1. La secuencia es grande, posiblemente infinita, o cara de producir entera.
  2. El consumidor podría parar antes del final (un break en la primera coincidencia, por ejemplo).
  3. Quieres encadenar transformaciones — filtrar, mapear, coger — sin construir listas intermedias.

Y cuándo no:

  • Necesitas acceso aleatorio (seq[42]). Los generadores solo van hacia adelante.
  • Necesitas iterar la misma secuencia varias veces. Usa una lista.
  • La secuencia es pequeña y ya la tienes. Una list comprehension es más simple.

Los generadores, las list comprehensions y las listas planas son cada uno la respuesta correcta para trabajos distintos. La habilidad es elegir uno sin pensarlo demasiado — y la forma más rápida de desarrollar ese instinto es fijarte, por cada iteración que escribas, en si "produce todo primero" o "produce uno a la vez" encaja mejor.

Siguiente: context managers en profundidad

Ya has visto la mayoría de los idiomatismos que usa Python para iterar. Los context managers — la sentencia with — vienen ahora, y se emparejan bien con los generadores para sacar datos en streaming desde archivos y conexiones de red.

Preguntas frecuentes

¿Qué es un generador en Python?

Un generador es una función que produce valores uno a uno, pausando entre ellos. Lo escribes con def como una función normal, pero usas yield en vez de return. Llamarlo devuelve un objeto generador; cada iteración de for o cada llamada a next() ejecuta la función hasta el siguiente yield.

¿Cuál es la diferencia entre una lista y un generador?

Una lista guarda cada elemento en memoria a la vez. Un generador computa elementos bajo demanda y los olvida después de consumirlos. Para secuencias grandes o infinitas, los generadores usan una cantidad fija y pequeña de memoria; para resultados pequeños que necesitas repetidamente, una lista es mejor.

¿Puedo iterar un generador dos veces?

No. Un generador se agota después del primer pase completo — un segundo bucle for sobre él no produce nada. Si necesitas iterar más de una vez, llama a la función generadora de nuevo para obtener un generador fresco, o materializa los resultados en una lista.

Aprende a programar con Coddy

COMENZAR