En Python, los generadores son una forma poderosa y eficiente de crear iteradores personalizados que producen valores bajo demanda, en lugar de generarlos todos de una vez. Utilizando la palabra clave yield, un generador “congela” su estado entre llamadas, permitiendo procesar secuencias grandes sin consumir memoria excesiva. Imagina un generador como un chef que prepara platillos uno por uno según los pides, en vez de cocinar todo el banquete de antemano. En este capítulo, exploraremos la construcción básica de generadores, enfocándonos en cómo usar yield para iterar de manera lazy y eficiente, sin adentrarnos en características avanzadas.
¿Qué es un Generador y por Qué Deberías Usarlo?
Permíteme guiarte paso a paso, como un mentor que no da nada por sentado. Un generador en Python es una función especial que, en lugar de retornar un valor único con return, usa yield para producir una secuencia de valores uno a uno. Cuando llamas a esta función, no se ejecuta inmediatamente; en su lugar, devuelve un objeto generador que puedes iterar, como con un bucle for.
Piensa en una analogía cotidiana: imagina que estás contando ovejas para dormir. Con una lista normal, las contarías todas de golpe y las almacenarías en memoria (por ejemplo, [1, 2, 3, …, 1000]). Un generador, en cambio, cuenta una oveja cada vez que la necesitas, “durmiendo” entre conteos. Esto ahorra memoria y tiempo para secuencias infinitas o muy grandes.
Antes de ver código, asegúrate de entender esto: los generadores son iteradores lazy (perezosos), lo que significa que computan valores solo cuando se solicitan. Esto es clave para eficiencia en aplicaciones como procesamiento de datos grandes o streams infinitos.
Creando Tu Primer Generador con Yield
Comencemos con lo básico. Para crear un generador, define una función normal pero usa yield en lugar de return. Cada yield pausa la función y entrega un valor al llamador, preservando el estado para la próxima llamada.
Aquí va un ejemplo simple: un generador que produce números pares. Guarda este código en un archivo llamado pares.py.
# Función generadora que produce números pares hasta un límite dado
def generador_pares(limite):
numero = 0 # Iniciamos en 0
while numero < limite:
yield numero # Pausamos y entregamos el número par actual
numero += 2 # Incrementamos para el próximo par
# Para ejecutarlo, creamos el generador y lo iteramos
pares = generador_pares(10) # Crea el objeto generador, no ejecuta la función aún
for par in pares: # Itera sobre el generador, llamando next() implícitamente
print(par) # Imprime: 0, 2, 4, 6, 8
PythonPara ejecutar este ejemplo, abre tu terminal y corre python pares.py. Verás los números pares impresos uno por uno. Nota cómo el generador no genera toda la lista de antemano; cada iteración del bucle for avanza el generador con next() internamente, pausando en cada yield.
Explicación paso a paso:
- Llamas a
generador_pares(10), lo que devuelve un objeto generador sin ejecutar el código interno. - El bucle for invoca implícitamente next(pares) en cada iteración.
- En la primera llamada, la función corre hasta el primer yield, entrega 0 y se pausa.
- En la siguiente, retoma desde donde quedó (numero=0), incrementa a 2, y yield lo entrega.
- Esto continúa hasta que el while termina, momento en que el generador levanta StopIteration para signaling fin.
Si intentas iterar de nuevo sobre el mismo objeto pares, no producirá nada porque los generadores se agotan después de una iteración completa. Crea uno nuevo si lo necesitas.
Diferencias Clave entre Generadores y Funciones Normales
No avancemos sin aclarar esto completamente. Una función normal ejecuta todo su código y retorna una vez con return, liberando su estado. Un generador, por el contrario, puede “retornar” múltiples veces vía yield, manteniendo variables locales entre pausas.
Analogía: una función normal es como un libro que lees de principio a fin en una sentada. Un generador es como un audiolibro que pausas y resumes en cualquier capítulo.
Veamos un ejemplo comparativo. Primero, una función normal que retorna una lista:
# Función normal: genera toda la lista de una vez
def lista_pares(limite):
pares = []
numero = 0
while numero < limite:
pares.append(numero)
numero += 2
return pares # Retorna la lista completa
# Uso
print(lista_pares(10)) # [0, 2, 4, 6, 8] – todo en memoria
PythonAhora, el generador equivalente (del ejemplo anterior). El generador usa menos memoria porque no almacena la lista; solo produce valores on-demand.
Ejecuta esto en un archivo comparacion.py con python comparacion.py para ver la diferencia en acción. Si tu límite es un millón, la función normal consumiría memoria para una lista enorme, mientras que el generador la maneja sin problemas.
Recuerda: los generadores no son listas; no puedes indexarlos como pares[0]. Úsalos donde necesites iteración secuencial.
Expresiones de Generador: Una Forma Concisa
Una vez que domines las funciones generadoras, explora las expresiones de generador, que son como list comprehensions pero con paréntesis y lazy evaluation.
Sintaxis: (expresion for item in iterable if condicion).
Ejemplo: genera cuadrados de números pares menores a 10.
# Expresión de generador
cuadrados_pares = (x**2 for x in range(10) if x % 2 == 0)
# Iteramos
for cuadrado in cuadrados_pares:
print(cuadrado) # Imprime: 0, 4, 16, 36, 64
PythonGuarda en cuadrados.py y ejecuta con python cuadrados.py. Es compacto y eficiente: no crea una lista intermedia.
Paso a paso:
- La expresión crea un generador anónimo.
- El for itera, evaluando cada
x**2solo cuando se necesita. - La condición
iffiltra en tiempo real.
Usa esto para pipelines de datos donde la eficiencia importa. No es una función, pero actúa como generador.
Manejo de Generadores Infinitos y Errores Comunes
Los generadores brillan con secuencias infinitas, algo imposible con listas. Por ejemplo, un generador de números naturales infinitos:
# Generador infinito de números naturales
def numeros_naturales():
n = 1
while True: # Bucle infinito
yield n # Entrega n y pausa
n += 1 # Incrementa
# Uso controlado
naturales = numeros_naturales()
for _ in range(5): # Limitamos manualmente
print(next(naturales)) # Imprime 1 a 5
PythonEjecuta en infinitos.py con python infinitos.py. Aquí usamos next() explícitamente para avanzar. Sin un límite, iteraría para siempre – un error común. Siempre controla infinitos con breaks o límites.
Otro error: olvidar que los generadores se agotan. Si intentas list(naturales) sin límite, colgará tu programa. Sé exigente: prueba con datos pequeños primero.
Aplicaciones Prácticas y Mejores Prácticas
Ahora que has construido bases sólidas, veamos usos reales. Generadores son ideales para leer archivos grandes línea por línea, procesar streams de datos (como logs), o simular corrientes infinitas en juegos/simulaciones.
Mejor práctica: Usa generadores cuando la memoria es crítica o la secuencia no necesita almacenarse completa. Combínalos con itertools para potencia extra, pero mantengámoslo básico aquí.
Recuerda repetir: yield no es return; pausa, no termina. Domina esto practicando: escribe tu propio generador para Fibonacci y ejecútalo.
Resumen del Capítulo
- Definición básica: Los generadores son funciones que usan yield para producir valores bajo demanda, creando iteradores lazy que ahorran memoria.
- Creación: Define una función con yield; llama para obtener un objeto generador, e itera con for o next().
- Diferencias con funciones normales: Pausan y preservan estado entre yields, a diferencia de return que termina la ejecución.
- Expresiones de generador: Forma concisa como
(x for x in iterable), similar a list comprehensions pero lazy. - Infinitos y errores: Maneja bucles infinitos con cuidado; generadores se agotan tras iteración completa.
- Aplicaciones: Útiles para datos grandes, streams, y eficiencia; practica con ejemplos como pares o Fibonacci para dominar.