PythonAprende PythonDocumentación

Iteradores y generadores

Produce valores uno a la vez, perezosamente: memoria eficiente.

Imagina procesar un archivo de 10 millones de líneas. Cargarlo todo en una lista consumiría gigas de memoria. Un generador produce cada valor justo cuando se necesita: memoria constante, trabajo diferido.

Iteradores: el protocolo detrás del for

Cuando escribes for x in lista, Python le pide a la lista un iterador y le va pidiendo valores con next() hasta que recibe la señal de fin (StopIteration):

numeros = iter([1, 2, 3])   # obtener un iterador manualmente

print(next(numeros))   # 1
print(next(numeros))   # 2
print(next(numeros))   # 3
next(numeros)          # StopIteration → agotado

Un iterador es de un solo uso: agotado, no se reinicia.

Generadores: funciones con yield

Un generador es una función que en vez de return usa yield: produce valores uno a la vez y congela su estado entre llamada y llamada:

def contar_hasta(n):
    i = 1
    while i <= n:
        yield i        # entrega el valor y PAUSA aquí
        i += 1         # reanuda aquí en el próximo next()

gen = contar_hasta(3)
print(next(gen))   # 1
print(next(gen))   # 2

for numero in contar_hasta(3):   # el for consume el generador
    print(numero)

La diferencia clave con una lista: los valores no existen todos a la vez. Se fabrican bajo demanda.

Compara el consumo

def primeros_millones_lista():
    return [x for x in range(10_000_000)]     # ~400 MB en memoria

def primeros_millones_gen():
    for x in range(10_000_000):
        yield x                                # bytes en memoria

range() mismo es perezoso — por eso puedes hacer for i in range(10**9) sin morir.

Expresiones generadoras

Como una list comprehension pero con paréntesis:

cuadrados_lista = [x ** 2 for x in range(5)]   # lista: todo ya calculado
cuadrados_gen = (x ** 2 for x in range(5))     # generador: bajo demanda

print(sum(x ** 2 for x in range(1_000_000)))   # ¡sin crear lista intermedia!

sum(), max(), any(), all() aceptan generadores directamente.

yield en cadena: pipelines

Los generadores se componen como tuberías:

def leer_lineas(texto):
    for linea in texto.split("\n"):
        yield linea

def solo_comentarios(lineas):
    for linea in lineas:
        if linea.strip().startswith("#"):
            yield linea

codigo = "# config\nx = 1\n# fin"
for comentario in solo_comentarios(leer_lineas(codigo)):
    print(comentario)     # # config / # fin

Cada etapa produce de a un elemento: el pipeline nunca carga todo en memoria.

Resumen

  • for consume iteradores con next() hasta StopIteration
  • Un generador (yield) produce valores perezosamente recordando su estado
  • Memoria constante vs listas que cargan todo
  • (x for x in ...) es la expresión generadora — ideal con sum/max/any
  • Generadores de un solo uso; si necesitas recorrer dos veces, crea uno nuevo

Quiz

  1. 1. ¿Qué caracteriza a un generador (una función con yield)?

  2. 2. ¿Qué ocurre al pedir next() a un generador ya agotado?

  3. 3. ¿Cuál es la ventaja principal de un generador frente a una lista?

Ejercicios

Ejercicio 1: Generador de Fibonacci

Completa el generador fib_gen(n) que produzca los primeros n números de Fibonacci con yield. Imprime list(fib_gen(6)).

Cargando editor…

Ejercicio 2: Pares perezosos

Completa el generador `pares(limite)` que produzca los números pares menores que limite, uno por uno con yield.

Cargando editor…