Skip to content

📙 Clase 25 — Generadores y yield

Fase 5 · Python intermedio / "pro" ⬅️ Volver al índice de clases

🎯 Qué aprendí

  • yield: funciones que producen valores de a uno, pausándose entre cada uno.
  • Por qué un generador de 1 millón de valores ocupa 200 bytes (medido).
  • Generator expressions: (x*x for x in ...) — la comprehension "perezosa".

📖 PARTE TEÓRICA

🐌 1. El problema: crear TODO cuando solo necesitas de a UNO

Una lista materializa todos sus elementos en memoria de una vez. Un generador los produce bajo demanda:

python
import sys
lista = [x for x in range(1_000_000)]     # el millón COMPLETO en RAM
gen   = (x for x in range(1_000_000))     # una "receta" que produce de a uno

print(f"{sys.getsizeof(lista):,} bytes")  # 8,448,728 bytes  (~8 MB)
print(sys.getsizeof(gen), "bytes")        # 200 bytes        (¡medido en tu venv!)

⏸️ 2. yield: la función que se pausa

Una función con yield no se ejecuta al llamarla: devuelve un generador. Cada next() la corre hasta el próximo yield y la deja pausada ahí, con su estado vivo.

python
def cuenta_regresiva(n):
    while n > 0:
        yield n          # ENTREGA el valor y SE PAUSA aquí
        n -= 1           # continúa al pedir el siguiente

g = cuenta_regresiva(3)
print(type(g).__name__)   # generator  ← no corrió nada todavía
print(next(g))            # 3   (corre hasta el yield)
print(next(g))            # 2   (despierta, da otra vuelta, se pausa)
print(next(g))            # 1
print(next(g))            # 💥 StopIteration: se agotó
llamada        estado interno
next(g) ──▶  corre hasta yield ──▶ entrega 3 ──▶ ⏸️ pausa (recuerda n=3)
next(g) ──▶  sigue: n=2, yield  ─▶ entrega 2 ──▶ ⏸️ pausa

En la práctica no usas next() a mano: el for lo hace por ti (y atrapa el StopIteration solo):

python
for x in cuenta_regresiva(3):
    print(x)              # 3, 2, 1

💡 Los generadores pueden ser infinitos — imposible con una lista:

python
def pares():
    n = 0
    while True:
        yield n
        n += 2

Se consume lo que se necesita y ya (next(p) → 0, 2, 4…).

🥤 3. Generator expressions

La comprehension con paréntesis en vez de corchetes es un generador. Y viene gratis con las funciones que consumen iterables:

python
gen = (x * x for x in range(4))
print(list(gen))                  # [0, 1, 4, 9]

print(sum(x * x for x in range(4)))    # 14  ← sin corchetes: no crea la lista

⚠️ Un generador se agota: tras recorrerlo una vez, queda vacío (la lista se puede recorrer mil veces). Si necesitas los datos dos veces → lista.

🧪 Tip de entrevista: "¿Diferencia entre return y yield?" → return termina la función y entrega un valor; yield la pausa y entrega un valor cada vez, la función se reanuda donde quedó. Con yield, llamar la función devuelve un generador.


🖥️ EN TU APP DE ESCRITORIO

Aunque el caso "1 millón de tareas" no es tu To-Do 😄, ya consumes generadores sin saberlo y tienes usos reales:

python
# ① Ya lo hacías: sum(...) sobre genexp — tu property de la Clase 16:
@property
def pendientes(self):
    return sum(1 for t in self._tareas if not t.hecha)   # ¡genexp!

# ② Leer un archivo GRANDE línea a línea (el objeto archivo ES un generador):
def lineas_utiles(ruta):
    with open(ruta, encoding="utf-8") as f:
        for linea in f:                  # nunca carga todo el archivo
            if linea.strip():
                yield linea.strip()

# ③ Los widgets hijos que recorres al repintar:
for w in self.lista.winfo_children():    # iterable, se consume de a uno
    w.destroy()
En tu appEl generador evita
Importar un CSV de 100k filascargar todo a RAM antes de procesar
any(t.hecha for t in tareas)crear una lista solo para preguntar "¿alguna?"
Paginar resultados grandesmaterializar lo que el usuario nunca verá

💡 any(...) y all(...) + genexp = preguntas elegantes: any(t["hecha"] for t in tareas) se detiene en el primer True (ni siquiera revisa el resto).


🗄️ CON BASE DE DATOS (caso de uso)

El cursor de sqlite3 es un iterador perezoso: recorrerlo trae filas bajo demanda, sin cargar la tabla completa:

python
# fetchall() = lista (todo a RAM) · iterar el cursor = de a poco (estilo generador)
for fila in con.execute("SELECT * FROM ventas"):    # va trayendo de a una
    procesar(fila)

🗄️ Regla: si solo vas a recorrer una vez, itera el cursor; usa fetchall() cuando necesites la lista completa (contar, indexar, recorrer dos veces).


🏋️ EJERCICIOS CON SOLUCIÓN

Ejercicio 1 — Pares hasta n

Escribe un generador pares_hasta(n) y recórrelo con for para n=8.

Ver solución
python
def pares_hasta(n):
    x = 0
    while x <= n:
        yield x
        x += 2

print(list(pares_hasta(8)))    # [0, 2, 4, 6, 8]

Ejercicio 2 — Suma sin lista

Suma los cubos de 1 a 100 sin crear ninguna lista.

Ver solución
python
print(sum(x**3 for x in range(1, 101)))    # 25502500  (genexp, cero listas)

Ejercicio 3 — El generador agotado

¿Qué imprime y por qué?

python
g = (x for x in range(3))
print(list(g))
print(list(g))
Ver solución
python
# [0, 1, 2]
# []        ← el generador SE AGOTÓ en el primer list(); no "rebobina".
# Si necesitas recorrer dos veces: materializa una lista primero.

❓ Preguntas y respuestas (autoevaluación)

1. ¿Qué devuelve llamar a una función que contiene yield?

Un generador (no ejecuta el cuerpo todavía).

2. ¿Por qué un generador gasta tan poca memoria?

No materializa los valores: produce de a uno al pedirlo, recordando solo su estado.

3. ¿Qué pasa al recorrer un generador por segunda vez?

Nada — está agotado (a diferencia de una lista).

4. ¿Cuándo prefieres sum(x for x in ...) sobre sum([x for x in ...])?

Siempre que solo consumas una vez: la genexp evita crear la lista intermedia.


📎 Apuntes relacionados

  • Comprehensions (la versión materializada) → Clase 07
  • Archivos línea a línea → Clase 19

➡️ Siguiente

Clase 26 · Decoradores — funciones que envuelven funciones.