Generadores de Python y yield
Aprende los generadores de Python y la palabra clave yield con ejemplos claros sobre funciones generadoras, expresiones, send() y casos de uso reales.
Un generador es un tipo especial de iterador que produce valores de uno en uno, bajo demanda, en lugar de calcularlos todos de antemano. Los generadores se definen usando la sintaxis ordinaria de función con yield en lugar de return. Son la solución idiomática de Python para secuencias grandes o infinitas donde construir una lista completa desperdiciaría memoria o tiempo.
Este capítulo cubre la palabra clave yield, funciones generadoras versus listas, expresiones generadoras, el envío de valores a un generador, el encadenamiento de generadores y patrones del mundo real.
¿Qué es un generador?
Cuando Python llama a una función normal, ejecuta el cuerpo hasta completarse y devuelve un valor. Cuando Python llama a una función generadora, no ejecuta el cuerpo en absoluto — devuelve un objeto generador. Cada vez que llamas a next() sobre ese objeto, la ejecución se reanuda desde donde se pausó por última vez (la sentencia yield), continúa hasta el siguiente yield y se suspende de nuevo.
def count_up(start, stop):
while start <= stop:
yield start # pause here, emit the value
start += 1
gen = count_up(1, 3)
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
# next(gen) would now raise StopIterationMecánica clave:
- El cuerpo de la función no se ejecuta hasta la primera llamada a
next(). - Las variables locales y el puntero de instrucción se conservan entre llamadas.
- Cuando el cuerpo de la función termina (o encuentra un
returnsin valor), Python lanzaStopIterationautomáticamente. - Un bucle
forllama anext()por ti y se detiene limpiamente enStopIteration.
La palabra clave yield
yield es la única sintaxis que distingue una función generadora de una función normal. Puedes usar yield en cualquier lugar donde podría aparecer un return, incluyendo dentro de bucles, condicionales y bloques try/except.
yield vs return
return | yield | |
|---|---|---|
| Tipo de función | Normal | Generadora |
| Ejecución tras la llamada | Se ejecuta hasta completarse | Se pausa en yield |
| Estado entre llamadas | Descartado | Conservado |
| Valores múltiples | Uno (o una tupla) | Uno por yield, secuencialmente |
| Memoria para datos grandes | Almacena todos los valores | Almacena un valor a la vez |
yield suspende, no termina
def three_things():
print("about to yield first")
yield "first"
print("about to yield second")
yield "second"
print("about to yield third")
yield "third"
print("generator exhausted")
for item in three_things():
print("got:", item)Salida:
about to yield first
got: first
about to yield second
got: second
about to yield third
got: third
generator exhaustedObserva las sentencias print entre los yields — el código normal se ejecuta entre cada suspensión.
Funciones generadoras vs listas
Considera generar los primeros n números al cuadrado. Usando una lista:
def squares_list(n):
result = []
for i in range(1, n + 1):
result.append(i * i)
return result
print(squares_list(5)) # [1, 4, 9, 16, 25]Usando un generador:
def squares_gen(n):
for i in range(1, n + 1):
yield i * i
gen = squares_gen(5)
print(list(gen)) # [1, 4, 9, 16, 25]Ambos producen los mismos valores, pero la versión con generador:
- Usa memoria O(1) independientemente de
n(la versión con lista usa O(n)) - Comienza a producir valores inmediatamente, sin esperar a construir toda la colección
- Puede representar secuencias infinitas (una lista no puede)
Cuándo elegir un generador
Usa un generador cuando:
- Solo necesitas iterar una vez sobre los valores.
- La secuencia es suficientemente grande como para que almacenarla toda en memoria sea relevante.
- Estás construyendo una canalización de datos (un generador alimenta a otro).
- La secuencia es potencialmente infinita (p. ej., leer líneas de registro desde un archivo en vivo).
Usa una lista cuando:
- Necesitas acceso aleatorio por índice.
- Necesitas iterar la misma secuencia varias veces.
- Necesitas
len(), slicing o clasificación en el lugar.
Expresiones generadoras
Una expresión generadora es para los generadores lo que una comprensión de lista es para las listas. La sintaxis es idéntica excepto que se usan paréntesis en lugar de corchetes:
# List comprehension — builds the full list immediately
squares_list = [x * x for x in range(1, 6)]
# Generator expression — lazy, produces one value at a time
squares_gen = (x * x for x in range(1, 6))
print(type(squares_list)) # <class 'list'>
print(type(squares_gen)) # <class 'generator'>
print(list(squares_gen)) # [1, 4, 9, 16, 25]Las expresiones generadoras son más útiles cuando se pasan directamente a una función que consume un iterable:
total = sum(x * x for x in range(1, 101)) # sum of squares 1..100
print(total) # 338350No se necesitan paréntesis adicionales cuando la expresión generadora es el único argumento de una llamada a función.
Filtrado con expresiones generadoras
evens = (x for x in range(20) if x % 2 == 0)
print(list(evens)) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]Generadores infinitos
Dado que un generador produce valores de forma perezosa, puede representar una secuencia sin fin. El ejemplo clásico es un contador infinito:
def counter(start=0):
n = start
while True:
yield n
n += 1
gen = counter(10)
print(next(gen)) # 10
print(next(gen)) # 11
print(next(gen)) # 12Para consumir solo una parte de un generador infinito, usa itertools.islice o sal de un bucle con break:
import itertools
gen = counter(1)
first_five = list(itertools.islice(gen, 5))
print(first_five) # [1, 2, 3, 4, 5]Un generador infinito práctico — la sucesión de Fibonacci:
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
print([next(fib) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]yield from — Delegar a un sub-generador
yield from permite a un generador delegar en otro iterable, reenviando cada valor de forma transparente:
def first_part():
yield 1
yield 2
def second_part():
yield 3
yield 4
def combined():
yield from first_part()
yield from second_part()
print(list(combined())) # [1, 2, 3, 4]yield from también funciona con cualquier iterable, no solo con generadores:
def flatten(nested):
for sublist in nested:
yield from sublist
data = [[1, 2], [3, 4], [5, 6]]
print(list(flatten(data))) # [1, 2, 3, 4, 5, 6]yield from es más limpio que un bucle for anidado sobre el sub-iterable, y reenvía correctamente las llamadas send() y throw() al generador delegado (importante para patrones de corrutinas).
Envío de valores a un generador
Los generadores son canales bidireccionales. El método .send(value) reanuda el generador y pasa un valor de vuelta como resultado de la expresión yield:
def accumulator():
total = 0
while True:
value = yield total # yield sends total out; receives value in
if value is None:
break
total += value
gen = accumulator()
next(gen) # prime the generator (advance to first yield)
print(gen.send(10)) # 10
print(gen.send(20)) # 30
print(gen.send(5)) # 35Reglas para .send():
- Debes llamar a
next(gen)(ogen.send(None)) una vez para avanzar el generador hasta el primeryieldantes de poder enviar un valor que no seaNone. send(None)es equivalente anext().- El valor enviado se convierte en el resultado de la expresión
yielden el lado izquierdo.
Estado del generador y agotamiento
Un objeto generador tiene un ciclo de vida con cuatro estados:
| Estado | Descripción |
|---|---|
| Creado | Función generadora llamada, el cuerpo aún no ha comenzado |
| En ejecución | Ejecutándose actualmente (dentro de una llamada a next() o send()) |
| Suspendido | Pausado en un yield; se reanudará en el próximo next() |
| Cerrado | El cuerpo terminó o se llamó a .close(); lanza StopIteration |
Una vez agotado, iterar de nuevo sobre un generador no produce nada:
gen = (x for x in range(3))
print(list(gen)) # [0, 1, 2]
print(list(gen)) # [] — already exhaustedSi necesitas iterar la salida de un generador más de una vez, conviértelo primero a una lista o recrea el generador.
return dentro de un generador
Una sentencia return dentro de un generador termina la iteración limpiamente. El valor pasado a return se convierte en el atributo value de la excepción StopIteration (raramente usado directamente, pero importante para la delegación con yield from):
def limited():
yield 1
yield 2
return "done" # StopIteration.value = "done"
gen = limited()
print(next(gen)) # 1
print(next(gen)) # 2
try:
next(gen)
except StopIteration as e:
print(e.value) # donePatrones del mundo real
Lectura de un archivo grande línea a línea
def read_lines(filepath):
with open(filepath) as f:
for line in f:
yield line.rstrip("\n")
# Memory usage stays constant regardless of file size
for line in read_lines("/etc/hosts"):
if line.startswith("#"):
continue
print(line)Construcción de una canalización de datos
Los generadores se componen de forma natural en canalizaciones donde cada etapa transforma el flujo:
def integers(n):
for i in range(1, n + 1):
yield i
def only_even(nums):
for n in nums:
if n % 2 == 0:
yield n
def squared(nums):
for n in nums:
yield n * n
# Compose: even squares from 1..20
pipeline = squared(only_even(integers(20)))
print(list(pipeline))
# [4, 16, 36, 64, 100, 144, 196, 256, 324, 400]Cada etapa es perezosa — los valores fluyen por la canalización de uno en uno sin construir listas intermedias.
División de un iterable en fragmentos
def chunks(iterable, size):
chunk = []
for item in iterable:
chunk.append(item)
if len(chunk) == size:
yield chunk
chunk = []
if chunk:
yield chunk
data = list(range(10))
for batch in chunks(data, 3):
print(batch)
# [0, 1, 2]
# [3, 4, 5]
# [6, 7, 8]
# [9]Generadores vs iteradores vs comprensiones
| Característica | Clase iterador | Función generadora | Expresión generadora |
|---|---|---|---|
| Sintaxis | Clase con __iter__/__next__ | def + yield | (expr for x in ...) |
| Verbosidad | Alta | Baja | Muy baja |
| Gestión de estado | Manual | Automática | Automática |
| Lógica de múltiples sentencias | Sí | Sí | No (expresión única) |
| Secuencias infinitas | Sí | Sí | Sí |
| Legible para lógica compleja | Sí | Sí | No |
Para cualquier cosa más que una transformación o filtro simple, una función generadora es más legible que una expresión generadora. Para iteración con estado complejo, una función generadora casi siempre es preferible a escribir una clase iterador completa — consulta Python Iterators para el enfoque basado en clases.
Las expresiones generadoras se combinan de forma natural con las comprensiones de lista y las comprensiones de diccionario/conjunto. Los decoradores también pueden envolver funciones generadoras para añadir comportamiento de caché o rastreo.