W3docs

Generadores de Python y yield

Aprende los generadores de Python y la palabra clave yield con ejemplos claros sobre funciones generadoras, expresiones, send() y casos de uso reales.

Un generador es un tipo especial de iterador que produce valores de uno en uno, bajo demanda, en lugar de calcularlos todos de antemano. Los generadores se definen usando la sintaxis ordinaria de función con yield en lugar de return. Son la solución idiomática de Python para secuencias grandes o infinitas donde construir una lista completa desperdiciaría memoria o tiempo.

Este capítulo cubre la palabra clave yield, funciones generadoras versus listas, expresiones generadoras, el envío de valores a un generador, el encadenamiento de generadores y patrones del mundo real.

¿Qué es un generador?

Cuando Python llama a una función normal, ejecuta el cuerpo hasta completarse y devuelve un valor. Cuando Python llama a una función generadora, no ejecuta el cuerpo en absoluto — devuelve un objeto generador. Cada vez que llamas a next() sobre ese objeto, la ejecución se reanuda desde donde se pausó por última vez (la sentencia yield), continúa hasta el siguiente yield y se suspende de nuevo.

def count_up(start, stop):
    while start <= stop:
        yield start        # pause here, emit the value
        start += 1

gen = count_up(1, 3)
print(next(gen))   # 1
print(next(gen))   # 2
print(next(gen))   # 3
# next(gen) would now raise StopIteration

Mecánica clave:

  • El cuerpo de la función no se ejecuta hasta la primera llamada a next().
  • Las variables locales y el puntero de instrucción se conservan entre llamadas.
  • Cuando el cuerpo de la función termina (o encuentra un return sin valor), Python lanza StopIteration automáticamente.
  • Un bucle for llama a next() por ti y se detiene limpiamente en StopIteration.

La palabra clave yield

yield es la única sintaxis que distingue una función generadora de una función normal. Puedes usar yield en cualquier lugar donde podría aparecer un return, incluyendo dentro de bucles, condicionales y bloques try/except.

yield vs return

returnyield
Tipo de funciónNormalGeneradora
Ejecución tras la llamadaSe ejecuta hasta completarseSe pausa en yield
Estado entre llamadasDescartadoConservado
Valores múltiplesUno (o una tupla)Uno por yield, secuencialmente
Memoria para datos grandesAlmacena todos los valoresAlmacena un valor a la vez

yield suspende, no termina

def three_things():
    print("about to yield first")
    yield "first"
    print("about to yield second")
    yield "second"
    print("about to yield third")
    yield "third"
    print("generator exhausted")

for item in three_things():
    print("got:", item)

Salida:

about to yield first
got: first
about to yield second
got: second
about to yield third
got: third
generator exhausted

Observa las sentencias print entre los yields — el código normal se ejecuta entre cada suspensión.

Funciones generadoras vs listas

Considera generar los primeros n números al cuadrado. Usando una lista:

def squares_list(n):
    result = []
    for i in range(1, n + 1):
        result.append(i * i)
    return result

print(squares_list(5))   # [1, 4, 9, 16, 25]

Usando un generador:

def squares_gen(n):
    for i in range(1, n + 1):
        yield i * i

gen = squares_gen(5)
print(list(gen))         # [1, 4, 9, 16, 25]

Ambos producen los mismos valores, pero la versión con generador:

  • Usa memoria O(1) independientemente de n (la versión con lista usa O(n))
  • Comienza a producir valores inmediatamente, sin esperar a construir toda la colección
  • Puede representar secuencias infinitas (una lista no puede)

Cuándo elegir un generador

Usa un generador cuando:

  • Solo necesitas iterar una vez sobre los valores.
  • La secuencia es suficientemente grande como para que almacenarla toda en memoria sea relevante.
  • Estás construyendo una canalización de datos (un generador alimenta a otro).
  • La secuencia es potencialmente infinita (p. ej., leer líneas de registro desde un archivo en vivo).

Usa una lista cuando:

  • Necesitas acceso aleatorio por índice.
  • Necesitas iterar la misma secuencia varias veces.
  • Necesitas len(), slicing o clasificación en el lugar.

Expresiones generadoras

Una expresión generadora es para los generadores lo que una comprensión de lista es para las listas. La sintaxis es idéntica excepto que se usan paréntesis en lugar de corchetes:

# List comprehension — builds the full list immediately
squares_list = [x * x for x in range(1, 6)]

# Generator expression — lazy, produces one value at a time
squares_gen = (x * x for x in range(1, 6))

print(type(squares_list))   # <class 'list'>
print(type(squares_gen))    # <class 'generator'>

print(list(squares_gen))    # [1, 4, 9, 16, 25]

Las expresiones generadoras son más útiles cuando se pasan directamente a una función que consume un iterable:

total = sum(x * x for x in range(1, 101))   # sum of squares 1..100
print(total)   # 338350

No se necesitan paréntesis adicionales cuando la expresión generadora es el único argumento de una llamada a función.

Filtrado con expresiones generadoras

evens = (x for x in range(20) if x % 2 == 0)
print(list(evens))   # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Generadores infinitos

Dado que un generador produce valores de forma perezosa, puede representar una secuencia sin fin. El ejemplo clásico es un contador infinito:

def counter(start=0):
    n = start
    while True:
        yield n
        n += 1

gen = counter(10)
print(next(gen))   # 10
print(next(gen))   # 11
print(next(gen))   # 12

Para consumir solo una parte de un generador infinito, usa itertools.islice o sal de un bucle con break:

import itertools

gen = counter(1)
first_five = list(itertools.islice(gen, 5))
print(first_five)   # [1, 2, 3, 4, 5]

Un generador infinito práctico — la sucesión de Fibonacci:

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
print([next(fib) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

yield from — Delegar a un sub-generador

yield from permite a un generador delegar en otro iterable, reenviando cada valor de forma transparente:

def first_part():
    yield 1
    yield 2

def second_part():
    yield 3
    yield 4

def combined():
    yield from first_part()
    yield from second_part()

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

yield from también funciona con cualquier iterable, no solo con generadores:

def flatten(nested):
    for sublist in nested:
        yield from sublist

data = [[1, 2], [3, 4], [5, 6]]
print(list(flatten(data)))   # [1, 2, 3, 4, 5, 6]

yield from es más limpio que un bucle for anidado sobre el sub-iterable, y reenvía correctamente las llamadas send() y throw() al generador delegado (importante para patrones de corrutinas).

Envío de valores a un generador

Los generadores son canales bidireccionales. El método .send(value) reanuda el generador y pasa un valor de vuelta como resultado de la expresión yield:

def accumulator():
    total = 0
    while True:
        value = yield total   # yield sends total out; receives value in
        if value is None:
            break
        total += value

gen = accumulator()
next(gen)          # prime the generator (advance to first yield)
print(gen.send(10))   # 10
print(gen.send(20))   # 30
print(gen.send(5))    # 35

Reglas para .send():

  1. Debes llamar a next(gen) (o gen.send(None)) una vez para avanzar el generador hasta el primer yield antes de poder enviar un valor que no sea None.
  2. send(None) es equivalente a next().
  3. El valor enviado se convierte en el resultado de la expresión yield en el lado izquierdo.

Estado del generador y agotamiento

Un objeto generador tiene un ciclo de vida con cuatro estados:

EstadoDescripción
CreadoFunción generadora llamada, el cuerpo aún no ha comenzado
En ejecuciónEjecutándose actualmente (dentro de una llamada a next() o send())
SuspendidoPausado en un yield; se reanudará en el próximo next()
CerradoEl cuerpo terminó o se llamó a .close(); lanza StopIteration

Una vez agotado, iterar de nuevo sobre un generador no produce nada:

gen = (x for x in range(3))
print(list(gen))   # [0, 1, 2]
print(list(gen))   # []  — already exhausted

Si necesitas iterar la salida de un generador más de una vez, conviértelo primero a una lista o recrea el generador.

return dentro de un generador

Una sentencia return dentro de un generador termina la iteración limpiamente. El valor pasado a return se convierte en el atributo value de la excepción StopIteration (raramente usado directamente, pero importante para la delegación con yield from):

def limited():
    yield 1
    yield 2
    return "done"    # StopIteration.value = "done"

gen = limited()
print(next(gen))   # 1
print(next(gen))   # 2
try:
    next(gen)
except StopIteration as e:
    print(e.value)  # done

Patrones del mundo real

Lectura de un archivo grande línea a línea

def read_lines(filepath):
    with open(filepath) as f:
        for line in f:
            yield line.rstrip("\n")

# Memory usage stays constant regardless of file size
for line in read_lines("/etc/hosts"):
    if line.startswith("#"):
        continue
    print(line)

Construcción de una canalización de datos

Los generadores se componen de forma natural en canalizaciones donde cada etapa transforma el flujo:

def integers(n):
    for i in range(1, n + 1):
        yield i

def only_even(nums):
    for n in nums:
        if n % 2 == 0:
            yield n

def squared(nums):
    for n in nums:
        yield n * n

# Compose: even squares from 1..20
pipeline = squared(only_even(integers(20)))
print(list(pipeline))
# [4, 16, 36, 64, 100, 144, 196, 256, 324, 400]

Cada etapa es perezosa — los valores fluyen por la canalización de uno en uno sin construir listas intermedias.

División de un iterable en fragmentos

def chunks(iterable, size):
    chunk = []
    for item in iterable:
        chunk.append(item)
        if len(chunk) == size:
            yield chunk
            chunk = []
    if chunk:
        yield chunk

data = list(range(10))
for batch in chunks(data, 3):
    print(batch)
# [0, 1, 2]
# [3, 4, 5]
# [6, 7, 8]
# [9]

Generadores vs iteradores vs comprensiones

CaracterísticaClase iteradorFunción generadoraExpresión generadora
SintaxisClase con __iter__/__next__def + yield(expr for x in ...)
VerbosidadAltaBajaMuy baja
Gestión de estadoManualAutomáticaAutomática
Lógica de múltiples sentenciasNo (expresión única)
Secuencias infinitas
Legible para lógica complejaNo

Para cualquier cosa más que una transformación o filtro simple, una función generadora es más legible que una expresión generadora. Para iteración con estado complejo, una función generadora casi siempre es preferible a escribir una clase iterador completa — consulta Python Iterators para el enfoque basado en clases.

Las expresiones generadoras se combinan de forma natural con las comprensiones de lista y las comprensiones de diccionario/conjunto. Los decoradores también pueden envolver funciones generadoras para añadir comportamiento de caché o rastreo.

Práctica

Práctica
¿Cuáles de las siguientes afirmaciones sobre los generadores de Python son correctas?
¿Cuáles de las siguientes afirmaciones sobre los generadores de Python son correctas?
Was this page helpful?