Apariencia
📙 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 ──▶ ⏸️ pausaEn 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:
pythondef pares(): n = 0 while True: yield n n += 2Se 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
returnyyield?" →returntermina la función y entrega un valor;yieldla pausa y entrega un valor cada vez, la función se reanuda donde quedó. Conyield, 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 app | El generador evita |
|---|---|
| Importar un CSV de 100k filas | cargar todo a RAM antes de procesar |
any(t.hecha for t in tareas) | crear una lista solo para preguntar "¿alguna?" |
| Paginar resultados grandes | materializar lo que el usuario nunca verá |
💡
any(...)yall(...)+ genexp = preguntas elegantes:any(t["hecha"] for t in tareas)se detiene en el primerTrue(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
➡️ Siguiente
Clase 26 · Decoradores — funciones que envuelven funciones.