Generadores en Python: yield, lazy evaluation y cómo ahorrar memoria

Un generador es una función que produce valores uno a uno con yield en lugar de calcularlos todos y devolverlos con return. El resultado es que el generador solo computa el siguiente valor cuando se le pide, lo que permite procesar secuencias enormes o infinitas con memoria constante.

yield: la diferencia clave con return

# Función normal: crea toda la lista en memoria
def cuadrados_lista(n):
    return [x**2 for x in range(n)]

# Generador: produce un cuadrado cada vez que se le pide
def cuadrados_gen(n):
    for x in range(n):
        yield x**2

# Con n=1_000_000, la lista ocupa ~8 MB; el generador, ~200 bytes
import sys
lista = cuadrados_lista(1_000_000)
gen   = cuadrados_gen(1_000_000)
print(sys.getsizeof(lista))  # ~8 MB
print(sys.getsizeof(gen))    # 208 bytes

Cómo consumir un generador

def contar(inicio, fin):
    n = inicio
    while n <= fin:
        yield n
        n += 1

# Iteración directa
for numero in contar(1, 5):
    print(numero)  # 1, 2, 3, 4, 5

# Convertir a lista (consume todo el generador)
print(list(contar(1, 5)))  # [1, 2, 3, 4, 5]

# next() para pedir uno a uno
gen = contar(10, 12)
print(next(gen))  # 10
print(next(gen))  # 11
print(next(gen))  # 12
# next(gen)       # StopIteration

Pipelines de generadores

Los generadores se encadenan con coste de memoria mínimo: cada elemento viaja por toda la cadena antes de pasar al siguiente:

def leer_lineas(nombre_fichero):
    with open(nombre_fichero) as f:
        yield from f

def filtrar_errores(lineas):
    for linea in lineas:
        if "ERROR" in linea:
            yield linea.strip()

def limpiar(lineas):
    for linea in lineas:
        yield linea.lower()

# Pipeline: solo una línea en memoria a la vez
pipeline = limpiar(filtrar_errores(leer_lineas("servidor.log")))
for linea_error in pipeline:
    print(linea_error)

Secuencias infinitas

def fibonacci():
    a, b = 0, 1
    while True:       # bucle infinito: el generador nunca termina
        yield a
        a, b = b, a + b

# Tomar solo los primeros 10
from itertools import islice
print(list(islice(fibonacci(), 10)))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

send() y throw(): comunicación bidireccional

def acumulador():
    total = 0
    while True:
        valor = yield total   # yield devuelve total y recibe el siguiente valor
        if valor is None:
            break
        total += valor

gen = acumulador()
next(gen)           # avanzar hasta el primer yield (devuelve 0)
print(gen.send(10)) # 10
print(gen.send(5))  # 15
print(gen.send(20)) # 35

yield from: delegar en otro generador

def aplanar(lista_anidada):
    for elemento in lista_anidada:
        if isinstance(elemento, list):
            yield from aplanar(elemento)  # delega recursivamente
        else:
            yield elemento

datos = [1, [2, 3], [4, [5, 6]], 7]
print(list(aplanar(datos)))  # [1, 2, 3, 4, 5, 6, 7]

Usa generadores cuando proceses ficheros grandes, streams de datos o cualquier secuencia que no necesitas tener completa en memoria. Para secuencias finitas pequeñas donde vas a necesitar todos los valores, una lista comprehension sigue siendo más clara.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP