▷ Generadores y `yield` en Python — Lazy evaluation explicada 2026

Generadores y `yield` en Python — Lazy evaluation explicada

Si llevas un tiempo con Python, has visto la palabra yield en algún sitio (probablemente en una función con pinta extraña) y te has preguntado: “¿qué es esto y por qué es distinto a return?”. Lo entiendes a medias, lo evitas, y sigues escribiendo listas a la antigua.

Bien. Hoy se acaba.

Los generadores son una de las herramientas más elegantes de Python. Te permiten producir valores uno a uno bajo demanda en vez de calcularlos todos de golpe y guardarlos en memoria. Esto desbloquea cosas que con listas son imposibles: procesar ficheros gigantes sin cargarlos enteros, secuencias infinitas, pipelines de datos eficientes.

En esta entrada te enseño qué son, cómo funciona yield por dentro, cuándo te interesan, y los casos reales donde te ahorran memoria y tiempo. Con código copy-paste y los errores típicos.

La idea en 30 segundos

Una función normal calcula y devuelve todo de golpe:

def cuadrados_hasta(n):
    resultado = []
    for i in range(n):
        resultado.append(i * i)
    return resultado

cuadrados_hasta(5)
# [0, 1, 4, 9, 16]   ← lista entera en memoria

Una función generadora produce uno a uno, bajo demanda:

def cuadrados_hasta(n):
    for i in range(n):
        yield i * i

gen = cuadrados_hasta(5)
# <generator object cuadrados_hasta>   ← NO ha calculado nada todavía

next(gen)   # 0
next(gen)   # 1
next(gen)   # 4
# ...

yield en lugar de return. Eso es todo. La función deja de ser una función normal y pasa a ser una función generadora. Cuando la llamas, te devuelve un objeto generador (no los valores), y los valores aparecen uno a uno cuando los pides con next() o iterando.

Por qué te importa: memoria y tiempo

Compara:

# Versión lista
suma = sum([i * i for i in range(10_000_000)])
# Construye lista de 10 millones de elementos en memoria.

# Versión generador
suma = sum(i * i for i in range(10_000_000))
# Calcula uno a uno, sin almacenar nada.

La diferencia: la primera reserva ~80 MB para la lista. La segunda usa prácticamente cero memoria (un valor a la vez). Para datasets pequeños, da igual. Para cosas serias, enorme.

Y otro plus: si solo necesitas los primeros 5 elementos, el generador solo calcula esos 5. La lista calcula los 10 millones igual.

💡 ¿Vienes de list comprehensions? Una generator expression es una list comp con paréntesis en vez de corchetes. Misma sintaxis, evaluación lazy.

Las dos formas de crear un generador

1. Función con yield

def contador(inicio=0):
    n = inicio
    while True:                  # ← infinito, pero no pasa nada
        yield n
        n += 1

c = contador()
print(next(c))   # 0
print(next(c))   # 1
print(next(c))   # 2

Sí, un generador puede ser infinito. Como no calcula valores hasta que los pides, no importa.

Tip-friki: un generador no es un iterador “limitado por la lista”. Cada yield pausa la función, devuelve el valor, y la próxima next() la reanuda exactamente donde se quedó con todas sus variables intactas. La función mantiene estado entre llamadas. Eso es lo nuevo.

2. Generator expression

cuadrados = (i * i for i in range(10))
# <generator object <genexpr>>

print(list(cuadrados))   # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Mismo formato que list comp, pero con paréntesis. Útil cuando el generador es simple y solo lo vas a pasar a otra función.

📥 Llévate el cheatsheet de Python (gratis)

PDF de 6 páginas con lo esencial: tipos, condicionales, bucles, estructuras de datos, funciones y los errores que más vas a cometer. Para tener al lado mientras programas.

Sin spam. Te apuntas a la lista, descargas el cheatsheet y recibes contenido de Python cada semana.

Cómo iterar un generador

Tres formas equivalentes:

def numeros():
    yield 1
    yield 2
    yield 3

# 1. for loop (lo más común)
for n in numeros():
    print(n)

# 2. next() manual
g = numeros()
print(next(g))   # 1
print(next(g))   # 2
print(next(g))   # 3
print(next(g))   # ❌ StopIteration

# 3. Convertir a lista (ojo: cargas todo a memoria)
print(list(numeros()))   # [1, 2, 3]

⚠️ Cuidado: una vez consumido un generador, no se rebobina. Si quieres iterarlo de nuevo, lo creas otra vez:

python
g = numeros()
list(g) # [1, 2, 3]
list(g) # [] ← ya está agotado

yield from — delegar a otro generador

Patrón muy útil cuando un generador llama a otro:

def primeros_naturales():
    yield 1
    yield 2
    yield 3

def todos():
    yield from primeros_naturales()
    yield 4
    yield 5

print(list(todos()))   # [1, 2, 3, 4, 5]

yield from gen es equivalente a for v in gen: yield v, pero más limpio. Útil para componer generadores.

Casos reales típicos

Leer un fichero gigante línea a línea

def lineas_de(fichero):
    with open(fichero, encoding="utf-8") as f:
        for linea in f:
            yield linea.rstrip()

# Uso
for linea in lineas_de("log_enorme.txt"):
    if "ERROR" in linea:
        print(linea)

Esto procesa un fichero de 20 GB sin problemas — solo carga una línea a la vez en memoria. Con f.readlines() o f.read() te quedas sin RAM.

💡 De hecho for linea in f ya es lazy por defecto. El generador externo añade una capa para reutilizarlo en distintos contextos.

Pipelines de datos

def numeros(n):
    for i in range(n):
        yield i

def pares(gen):
    for x in gen:
        if x % 2 == 0:
            yield x

def cuadrados(gen):
    for x in gen:
        yield x * x

# Composición lazy
resultado = cuadrados(pares(numeros(20)))
print(list(resultado))   # [0, 4, 16, 36, 64, 100, 144, 196, 256, 324]

Cada paso aplica su lógica a un valor a la vez. Memoria mínima, código limpio.

Generador infinito de IDs únicos

def generador_ids(prefijo="ID"):
    contador = 0
    while True:
        contador += 1
        yield f"{prefijo}-{contador:06d}"

ids = generador_ids("MOVIE")
print(next(ids))   # MOVIE-000001
print(next(ids))   # MOVIE-000002
print(next(ids))   # MOVIE-000003

💡 Patrón típico cuando inicializas datos en bases de datos o tests.

Procesar páginas de una API hasta agotar resultados

import requests

def paginar(url):
    while url:
        r = requests.get(url)
        r.raise_for_status()
        datos = r.json()
        yield from datos["resultados"]
        url = datos.get("siguiente")   # None cuando se acaba

for resultado in paginar("https://api.example.com/v1/items"):
    print(resultado)

El consumidor itera tranquilamente sin saber cuántas peticiones HTTP hay detrás. El generador encapsula la paginación.

💡 ¿APIs y JSON? Mira JSON en Python.

Generador vs lista — cuándo cuál

Quieres… Usa…
Iterar una vez Generador (más eficiente)
Iterar varias veces Lista (los gens se agotan)
Indexar (mi_lista[5]) Lista (los gens no soportan [])
Conocer la longitud (len(...)) Lista (los gens no tienen length)
Procesar fichero/secuencia gigante Generador
Encadenar transformaciones lazy Generador
Pasar a sum, max, min, any, all Generator expression (sin paréntesis extra)
Pasar a sorted, len Lista

Tip-friki: sum(i*i for i in range(100)) no necesita los paréntesis exteriores. Cuando una generator expression es el único argumento de una función, los paréntesis se omiten.

Errores típicos al usar generadores

# 1. Reusar un generador agotado
g = (i for i in range(3))
list(g)   # [0, 1, 2]
list(g)   # []  ← ya está agotado

# 2. Asumir que tiene len
len(g)    # ❌ TypeError: object of type 'generator' has no len()
sum(1 for _ in g)   # cuenta consumiendo (y agota el gen)

# 3. Indexar
g[0]      # ❌ TypeError: 'generator' object is not subscriptable
list(g)[0]   # ✓ pero te cargas la ventaja de memoria

# 4. Mezclar yield y return con valor en la misma función
def malo():
    yield 1
    return 2   # ❌ en gens, return solo señala fin (StopIteration); el valor 2 se ignora en for-loop

# 5. Olvidar que un generador es lazy y modificar lo que itera
nums = [1, 2, 3, 4]
gen = (n * 2 for n in nums)
nums.append(5)
list(gen)   # [2, 4, 6, 8, 10] ← el 5 se incluye porque el gen no se evaluó hasta list()

# 6. Querer reusarlo y descubrir que se agotó
def total_y_max(gen):
    return sum(gen), max(gen)   # ❌ max recibe el gen ya agotado por sum
# ✓ convierte primero a lista si necesitas dos pasadas:
def total_y_max(gen):
    datos = list(gen)
    return sum(datos), max(datos)

Resumen

Concepto Regla
Función generadora Tiene yield en algún sitio. Devuelve un generador, no valores.
Generator expression Sintaxis (expr for x in iterable). Lazy.
yield Pausa la función y devuelve un valor. Reanuda en la siguiente next().
yield from gen Delega: produce todos los valores de otro generador.
Cuándo usar Datasets grandes, secuencias infinitas, pipelines, lazy evaluation.
Cuándo NO Necesitas indexar, len(), iterar varias veces, dataset pequeño.
Truco final Como argumento único de funciones (sum, max, any, all), omite paréntesis externos.

¿Te ha valido esto?

Si te ha resultado útil, llévate el cheatsheet de Python en PDF — 6 páginas con tipos, condicionales, bucles, estructuras de datos, funciones y los errores típicos. Para tener al lado mientras programas. Gratis.

Sin spam. Email + cheatsheet + un correo por semana con tutoriales nuevos.

Tu siguiente paso

Si has llegado hasta aquí, los generadores ya no son magia rara para ti. La próxima vez que necesites procesar un fichero gigante, encadenar transformaciones, o producir secuencias infinitas — sabes que yield está esperándote.

Si quieres aprender Python desde la base hasta proyectos reales con datos, ficheros y APIs, en El Pythonista lo enseño paso a paso.

Un abrazo,
Oscar

¿Quieres aprender Python en orden, no a saltos?

Esto que has leído es solo una pieza. En El Pythonista lo verás todo encadenado: 11 módulos, 37+ horas de vídeo, 734 actividades y un proyecto real (MovieTracker) que crece contigo desde la primera variable hasta el deploy a producción.

Ver el curso completo →

37+ horas · 734 actividades · Proyecto real · Acceso de por vida · 14 días de garantía

Compartir

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Publicar un comentario