Saltar al contenido
iteradores-y-generadores.py · devschool

Iteradores y generadores en Python

Lección 15 de 31 · 12 min de lectura · Actualizado el

En esta lección
  1. Iterables e iteradores
  2. Crear tu propio iterador
  3. Generadores con yield
  4. Evaluación perezosa: ahorrar memoria
  5. Expresiones generadoras
  6. Encadenar generadores: tuberías de datos
  7. yield from: delegar en otro iterable
  8. itertools: generadores ya hechos
  9. Cuándo usar cada cosa
  10. Errores frecuentes
  11. Resumen

Llevas tiempo usando for para recorrer listas, cadenas, diccionarios y rangos. Pero ¿cómo sabe for recorrer cosas tan distintas? La respuesta son los iteradores, un mecanismo muy sencillo que hay detrás de cada bucle. En esta lección verás cómo funcionan, cómo crear los tuyos y, sobre todo, cómo escribir generadores con yield: funciones que producen valores de uno en uno, sin guardarlos todos en memoria. Son la herramienta de Python para trabajar con datos muy grandes o incluso infinitos.

Iterables e iteradores

Hay dos palabras parecidas que conviene separar bien:

  • Un iterable es cualquier cosa que se puede recorrer con for: listas, tuplas, cadenas, diccionarios, conjuntos, range, ficheros…
  • Un iterador es el objeto que hace el recorrido: sabe cuál es el siguiente elemento y recuerda por dónde va.

Piensa en un libro y un marcapáginas. El libro es el iterable: tiene todas las páginas. El marcapáginas es el iterador: indica por qué página vas. Puedes tener varios marcapáginas en el mismo libro, cada uno en una página distinta.

iter() y next()

La función iter() pide a un iterable un iterador nuevo, y next() le pide al iterador el siguiente elemento:

dias = ["lunes", "martes", "miércoles"]
marcador = iter(dias)

print(next(marcador))   # lunes
print(next(marcador))   # martes
print(next(marcador))   # miércoles

¿Y si pides uno más? El iterador avisa de que se ha terminado lanzando la excepción StopIteration:

try:
    next(marcador)
except StopIteration:
    print("No quedan elementos")
# No quedan elementos

next() admite un segundo argumento con el valor que devolverá cuando no queden elementos, en lugar de lanzar la excepción:

print(next(marcador, "fin"))   # fin

Un iterador solo avanza: no puede volver atrás ni empezar de nuevo. Cuando se agota, se queda agotado. Si quieres recorrer otra vez, pide un iterador nuevo con iter(dias).

Lo que hace for por dentro

Ahora puedes entender qué hace un bucle for. Este código:

for dia in dias:
    print(dia.upper())
# LUNES
# MARTES
# MIÉRCOLES

es, por dentro, equivalente a este:

iterador = iter(dias)
while True:
    try:
        dia = next(iterador)
    except StopIteration:
        break
    print(dia.upper())
# LUNES
# MARTES
# MIÉRCOLES

for llama a iter() una vez, después llama a next() en cada vuelta y termina cuando recibe StopIteration. Por eso funciona con cualquier objeto que sepa dar un iterador, sea del tipo que sea.

Crear tu propio iterador

Para que un objeto sea un iterador necesita dos métodos especiales:

  • __iter__(): devuelve el iterador. En un iterador, se devuelve a sí mismo (return self).
  • __next__(): devuelve el siguiente valor o lanza StopIteration cuando no quedan.

Vamos a crear un iterador que recorra los kilómetros de una ruta de senderismo de cinco en cinco:

class PuntosDeControl:
    def __init__(self, distancia_total, cada=5):
        self.distancia_total = distancia_total
        self.cada = cada
        self.actual = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.actual > self.distancia_total:
            raise StopIteration
        km = self.actual
        self.actual += self.cada
        return km

for km in PuntosDeControl(18):
    print(f"Control en el km {km}")
# Control en el km 0
# Control en el km 5
# Control en el km 10
# Control en el km 15

Funciona, pero hay que escribir bastante: una clase, dos métodos y llevar el estado (self.actual) a mano. Además, como el objeto es su propio iterador, solo se puede recorrer una vez:

ruta = PuntosDeControl(10)
print(list(ruta))   # [0, 5, 10]
print(list(ruta))   # []  (ya está agotado)

Por suerte, casi nunca tendrás que escribir iteradores así. Python tiene una forma mucho más cómoda: los generadores.

Generadores con yield

Un generador es una función que, en lugar de return, usa yield (producir, ceder). Cada yield entrega un valor y pausa la función justo en ese punto. Cuando se pide el siguiente valor, la función continúa donde se quedó, con todas sus variables intactas.

El mismo iterador de antes, como generador:

def puntos_de_control(distancia_total, cada=5):
    km = 0
    while km <= distancia_total:
        yield km
        km += cada

for km in puntos_de_control(18):
    print(f"Control en el km {km}")
# Control en el km 0
# Control en el km 5
# Control en el km 10
# Control en el km 15

Cuatro líneas en lugar de una clase entera. Python crea por ti __iter__, __next__ y el StopIteration final, que se lanza solo cuando la función termina.

Paso a paso

Llamar a una función generadora no ejecuta su código: devuelve un objeto generador, que es un iterador. El código avanza cada vez que llamas a next():

def preparar_cafe():
    print("  (moliendo café)")
    yield "café molido"
    print("  (calentando agua)")
    yield "agua caliente"
    print("  (sirviendo)")
    yield "café listo"

cafetera = preparar_cafe()
print(type(cafetera).__name__)   # generator
print(next(cafetera))
print(next(cafetera))
print(next(cafetera))
# generator
#   (moliendo café)
# café molido
#   (calentando agua)
# agua caliente
#   (sirviendo)
# café listo

Fíjate en el orden: el primer print de la función no aparece hasta el primer next(). Entre un next() y otro, la función está “congelada” esperando.

yield frente a return

returnyield
Qué devuelveUn valor y la función terminaUn valor y la función se pausa
Cuántas vecesUnaTantas como quieras
Resultado de llamar a la funciónEl valorUn generador
MemoriaTodo el resultado de golpeUn valor cada vez

Un generador también puede tener return (sin valor): sirve para terminar antes de tiempo.

Evaluación perezosa: ahorrar memoria

Los generadores son perezosos: no calculan un valor hasta que alguien lo pide. Esto tiene una consecuencia enorme en el uso de memoria. Compara una lista de un millón de números con un generador que produce los mismos números:

import sys

lista = [n * n for n in range(1_000_000)]
generador = (n * n for n in range(1_000_000))

print(sys.getsizeof(lista) > 1_000_000)   # True: varios MB
print(sys.getsizeof(generador) < 500)     # True: unos pocos bytes
print(sum(generador))                     # 333332833333500000

sys.getsizeof da el tamaño de un objeto en bytes (el número exacto depende de la versión de Python). La lista guarda el millón de resultados; el generador solo guarda cómo calcular el siguiente. sum va pidiéndole valores de uno en uno y nunca hay más de uno en memoria.

Esto es lo que hace Python al leer un fichero línea a línea con for linea in fichero: no carga el fichero entero, sino una línea cada vez. Así puedes procesar ficheros de varios gigas.

Generadores infinitos

Como solo se calcula lo que se pide, un generador puede no terminar nunca. Por ejemplo, números de ticket para la cola de una carnicería:

def turnos(prefijo):
    numero = 1
    while True:
        yield f"{prefijo}{numero:03d}"
        numero += 1

cola = turnos("C")
print(next(cola), next(cola), next(cola))   # C001 C002 C003

El while True no bloquea el programa, porque la función se pausa en cada yield. Eso sí: nunca hagas list(turnos("C")), porque intentaría crear una lista infinita.

Expresiones generadoras

Ya viste una arriba: (n * n for n in range(...)). Una expresión generadora se escribe igual que una comprensión de lista, pero con paréntesis en lugar de corchetes. Crea un generador sin necesidad de escribir una función:

precios = [12.5, 3.99, 45.0, 7.25, 19.9]

caros = (p for p in precios if p > 10)
print(caros)              # <generator object <genexpr> at 0x...>
print(list(caros))        # [12.5, 45.0, 19.9]

print(round(sum(p * 1.21 for p in precios), 2))   # 107.25
print(max(len(d) for d in dias))                  # 9
print(any(p > 40 for p in precios))               # True

Cuando la expresión es el único argumento de una función, como en sum(...), puedes omitir los paréntesis extra. Úsalas cuando vayas a recorrer los datos una sola vez (sumar, buscar el máximo, comprobar con any o all): ahorran memoria y no crean una lista que nadie va a usar.

Cuidado: la dirección que muestra print(caros) (0x...) cambia en cada ejecución. Y recuerda que un generador se agota: después de list(caros), el generador caros ya está vacío.

Encadenar generadores: tuberías de datos

Como cada generador consume otro iterable, puedes conectarlos como piezas de una tubería. Cada paso procesa un elemento y se lo pasa al siguiente:

registro = [
    "2026-10-01;Ana;45",
    "2026-10-01;Luis;error",
    "",
    "2026-10-02;Marta;60",
    "2026-10-03;Ana;30",
]

def lineas_validas(lineas):
    for linea in lineas:
        if linea.strip():
            yield linea

def separar(lineas):
    for linea in lineas:
        fecha, nombre, minutos = linea.split(";")
        if minutos.isdigit():
            yield nombre, int(minutos)

entrenos = separar(lineas_validas(registro))
for nombre, minutos in entrenos:
    print(f"{nombre:<6} {minutos:>3} min")
# Ana     45 min
# Marta   60 min
# Ana     30 min

Si registro fuera un fichero de millones de líneas, el programa usaría la misma memoria: cada línea atraviesa la tubería entera antes de leer la siguiente.

yield from: delegar en otro iterable

Cuando un generador quiere producir todos los valores de otro iterable, puede usar yield from en lugar de un bucle:

def todas_las_tareas(proyectos):
    for tareas in proyectos.values():
        yield from tareas      # equivale a: for t in tareas: yield t

proyectos = {
    "web": ["maquetar", "formulario"],
    "app": ["login", "notificaciones", "pruebas"],
}
print(list(todas_las_tareas(proyectos)))
# ['maquetar', 'formulario', 'login', 'notificaciones', 'pruebas']

Es especialmente útil en generadores recursivos, como uno que recorre una lista anidada a cualquier profundidad (repasa la recursividad si lo necesitas):

def aplanar(datos):
    for elemento in datos:
        if isinstance(elemento, list):
            yield from aplanar(elemento)
        else:
            yield elemento

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

itertools: generadores ya hechos

El módulo itertools de la biblioteca estándar trae muchos iteradores listos para usar. Tres de los más útiles:

from itertools import count, islice, chain

# count: cuenta sin fin desde un número
for n in count(10, 5):
    if n > 25:
        break
    print(n, end=" ")
print()
# 10 15 20 25

# islice: corta un iterador como si fuera una lista (sin crearla)
print(list(islice(turnos("P"), 3)))        # ['P001', 'P002', 'P003']
print(list(islice(count(), 2, 10, 3)))     # [2, 5, 8]

# chain: encadena varios iterables como si fueran uno
mananas = ["yoga", "pilates"]
tardes = ["escalada", "boulder"]
for clase in chain(mananas, tardes):
    print(clase, end=" | ")
print()
# yoga | pilates | escalada | boulder | 

islice es la forma segura de sacar unos pocos elementos de un generador infinito, porque a los generadores no se les puede aplicar [:3]. Otras funciones prácticas son zip y enumerate, que ya conoces y que también son iteradores perezosos.

Cuándo usar cada cosa

Necesitas…Usa
Recorrer los datos varias veces o acceder por posiciónUna lista
Recorrer una vez, sobre todo con muchos datosUna expresión generadora
Una secuencia con lógica (estado, condiciones, varios pasos)Una función con yield
Un objeto con más métodos que, además, se pueda recorrerUna clase con __iter__ (que puede ser un generador)
Combinar, cortar o repetir iterablesitertools

Errores frecuentes

  • Recorrer un generador dos veces: la segunda vez no da nada, porque ya está agotado. Si necesitas los datos varias veces, guárdalos en una lista con list(...).
  • Esperar que la función generadora se ejecute al llamarla: preparar_cafe() no imprime nada; solo crea el generador. El código corre al pedir valores con next() o for.
  • Usar len() o índices con un generador: len(gen) y gen[0] dan TypeError. Los generadores no saben cuántos elementos tienen ni permiten saltar a una posición.
  • Convertir en lista un generador infinito: list(count()) no termina nunca. Usa islice o un break.
  • Confundir corchetes y paréntesis: [x for x in datos] crea una lista; (x for x in datos), un generador.
  • Olvidar StopIteration en __next__: un iterador escrito a mano sin esa excepción produce valores para siempre.

Resumen

ConceptoQué esEjemplo
IterableSe puede recorrer con forLista, cadena, range, fichero
IteradorDa el siguiente elemento y recuerda la posiciónit = iter(lista)
next(it)Pide el siguiente valornext(it, "fin")
StopIterationAviso de que no quedan elementosLa captura for por ti
__iter__ / __next__Métodos para crear un iterador propioreturn self / raise StopIteration
yieldEntrega un valor y pausa la funciónyield km
Expresión generadoraComprensión perezosa con paréntesissum(p for p in precios)
yield fromProduce todos los valores de otro iterableyield from tareas
count, islice, chainIteradores de itertoolsislice(gen, 3)

En la siguiente lección usarás funciones que envuelven a otras funciones: los decoradores.

Pon a prueba lo que has aprendido

[Python] ¿Qué imprime este programa, línea por línea?
def g():
    print("inicio")
    yield 1
    print("fin")

x = g()
print("creado")
print(next(x))

[Python] ¿Qué imprimen las dos líneas?
gen = (n * n for n in range(3))
print(list(gen))
print(list(gen))

[Python] ¿Qué imprime este código?
it = iter([10, 20])
print(next(it), next(it), next(it, "fin"))

[Python] count(10, 5) genera 10, 15, 20... sin fin. ¿Cuál es la forma segura de obtener solo sus tres primeros valores?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.