Iteradores y generadores en Python — práctica con Fibonacci sin fin, una tubería de primos y la cola de reservas de la biblioteca (y el portátil que se quedó pensando)

Para un ejercicio de clase necesitaba «unos cuantos» números de Fibonacci y, por si acaso, generé una lista con los primeros diez millones. El portátil se quedó pensando, el ventilador despegó y al final ni siquiera llegué a usarlos: solo necesitaba los que no pasaban de 100. La lección que me llevé es la de todo este artículo: no calcules por adelantado lo que no sabes si vas a necesitar; pide los valores de uno en uno y para cuando tengas bastante.

En la teoría de iteradores y generadores viste cómo funciona el for por dentro, por qué yield pausa en lugar de terminar y por qué un generador se gasta. Hoy lo ponemos a trabajar con tres programas de nivel medio: un generador infinito de Fibonacci, una tubería de generadores de números primos y la cola de reservas de un libro de la biblioteca, hecha con nodos enlazados y recorrida con un generador. Los tres usan datos que se parecen a los de verdad: secuencias que no terminan, límites que no sabes de antemano y una cola que cambia mientras la usas.

Antes de empezar

Conviene tener claro:

  • Que un for hace iter() y después next() hasta que salta StopIteration.
  • Que una función con yield devuelve un generador y no ejecuta nada hasta el primer next().
  • Que return dentro de un generador lo termina (y el for que lo recorre simplemente acaba).
  • Que un generador solo se recorre una vez, y una lista, las que quieras.

Si alguno te suena dudoso, repasa la teoría antes de seguir.

Los tres programas usan datos fijos (nada de input()) y no necesitan instalar nada: copia cada uno en un archivo .py y ejecútalo con python3 archivo.py. Las salidas que verás aquí son las reales, de una sola ejecución con Python 3.13.

Programa 1: generador de Fibonacci infinito

La sucesión de Fibonacci (0, 1, 1, 2, 3, 5, 8…) es el ejemplo perfecto de secuencia sin final: cada número es la suma de los dos anteriores. El programa tiene que dar los 10 primeros, los que no pasan de 100, los pares hasta 500, el primero que llega a 1000 (con su posición) y ver cómo el cociente entre dos seguidos se acerca a la razón áurea. Lo que lo complica: cada pregunta necesita una cantidad distinta de números, y no sabemos cuántos de antemano.

La idea

Un solo generador infinito, fibonacci(), que solo guarda dos números (el anterior y el actual) y entrega uno en cada next(). Todo lo demás son formas distintas de dejar de pedir: primeros(n, gen) corta por cantidad, hasta(limite, gen) corta por valor (con return), y un for con break corta por una condición cualquiera. El generador no sabe quién lo usa ni cuántos le van a pedir; solo sabe dar el siguiente.

Antes de programar, piensa en cómo pasar de (anterior, actual) = (0, 1) a (1, 1) y después a (1, 2) sin perder ningún valor por el camino.

El código

def fibonacci():
    """Generador infinito de la sucesión de Fibonacci: 0, 1, 1, 2, 3, 5..."""
    anterior, actual = 0, 1
    while True:
        yield anterior
        anterior, actual = actual, anterior + actual   # la derecha se calcula ANTES de asignar


def hasta(limite, gen):
    """Deja pasar los valores de gen mientras no superen el límite."""
    for x in gen:
        if x > limite:
            return                # return en un generador = se acabó (StopIteration)
        yield x


def primeros(n, gen):
    """Los n primeros valores de cualquier generador (como itertools.islice)."""
    for _ in range(n):
        yield next(gen)


print('Primeros 10:', list(primeros(10, fibonacci())))
print('Hasta 100:  ', list(hasta(100, fibonacci())))
pares = [n for n in hasta(500, fibonacci()) if n % 2 == 0]
print('Pares hasta 500:', pares)

for pos, n in enumerate(fibonacci()):          # enumerate también va pidiendo de uno en uno
    if n >= 1000:
        print(f'Primero >= 1000: {n} (posición {pos})')
        break

g = fibonacci()
next(g)                                        # saltamos el 0 para no dividir entre 0
a = next(g)
for i in range(1, 21):
    b = next(g)
    if i in (5, 10, 20):
        print(f'Cociente tras {i:2} pasos: {b / a:.6f}')
    a = b

La línea más importante es anterior, actual = actual, anterior + actual. Python calcula toda la parte derecha antes de asignar nada: primero forma la tupla (actual, anterior + actual) con los valores viejos y después la reparte. Si lo escribieras en dos líneas (anterior = actual y luego actual = anterior + actual), la segunda usaría el anterior ya cambiado y la sucesión saldría mal.

Fibonacci con un generadorSeis llamadas a next(): las parejas (0,1), (1,1), (1,2), (2,3), (3,5), (5,8) y los valores entregados 0, 1, 1, 2, 3, 5.Estado del generador (anterior, actual) en cada next()next 1(0, 1)yield 0next 2(1, 1)yield 1next 3(1, 2)yield 1next 4(2, 3)yield 2next 5(3, 5)yield 3next 6(5, 8)yield 5anterior, actual = actual, anterior + actualLa parte derecha (1, 0 + 1) se calcula entera ANTES de asignar: no hace falta variable temporal.
El generador solo guarda dos números, pidas 6 o 6 millones.

En el dibujo, cada píldora violeta es el estado del generador en un next() y la etiqueta coral es lo que entrega. El generador nunca guarda más de dos números.

Las dos funciones auxiliares también son generadores:

def hasta(limite, gen):
    """Deja pasar los valores de gen mientras no superen el límite."""
    for x in gen:
        if x > limite:
            return                # return en un generador = se acabó (StopIteration)
        yield x


def primeros(n, gen):
    """Los n primeros valores de cualquier generador (como itertools.islice)."""
    for _ in range(n):
        yield next(gen)
  • hasta recorre el generador que le pasan y, en cuanto un valor se pasa del límite, hace return. Eso termina hasta (y el list() que la recorre acaba), aunque fibonacci() podría seguir dando números para siempre.
  • primeros llama n veces a next(gen) y entrega cada valor. Es lo que hace itertools.islice(gen, n).

Salida

Primeros 10: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Hasta 100:   [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
Pares hasta 500: [0, 2, 8, 34, 144]
Primero >= 1000: 1597 (posición 17)
Cociente tras  5 pasos: 1.600000
Cociente tras 10 pasos: 1.618182
Cociente tras 20 pasos: 1.618034

Comprobémoslo a mano. Los diez primeros: 0, 1, 0 + 1 = 1, 1 + 1 = 2, 1 + 2 = 3, 2 + 3 = 5, 3 + 5 = 8, 13, 21, 34. Hasta 100 llegan dos más (55 y 89); el siguiente, 144, ya se pasa y hasta hace return. Los pares hasta 500 son 0, 2, 8, 34 y 144: el siguiente par sería 610 (en Fibonacci, uno de cada tres números es par). El primero que llega a 1000 es 1597 = 610 + 987, en la posición 17 contando desde 0. Y los cocientes 8 / 5 = 1,6, después 1,618182 y 1,618034: la razón áurea (1,6180339…) aparece sola.

next() nºanterioractualEntregaEstado siguiente
1010(1, 0 + 1) = (1, 1)
2111(1, 1 + 1) = (1, 2)
3121(2, 1 + 2) = (2, 3)
4232(3, 2 + 3) = (3, 5)
5353(5, 3 + 5) = (5, 8)

Preguntas que te puedes hacer

¿Por qué cada pregunta crea un fibonacci() nuevo? Porque un generador se gasta. Si reutilizáramos el mismo para «hasta 100» y después para «pares hasta 500», el segundo empezaría donde se quedó el primero (en 144) y nos saltaríamos el principio.

¿Por qué hasta usa return y no break? Dentro de un generador, return lo termina: es la forma limpia de decir «ya no hay más». Un break saldría del for interno y, como después no hay nada, el generador también acabaría; funcionaría igual, pero return deja más claro qué pasa.

¿Por qué saltamos el 0 antes de calcular los cocientes? Porque el cociente es b / a y el primer valor es 0: dividir entre 0 lanzaría ZeroDivisionError. Un next(g) suelto descarta ese primer valor.

¿Por qué enumerate(fibonacci()) no se queda colgado si el generador es infinito? Porque enumerate también es perezoso: pide de uno en uno. El break del for deja de pedir en la posición 17, y nadie calcula ni un número más.

list(fibonacci()). Sin un límite, list() intenta recorrer el generador entero, que no termina nunca: el programa se queda colgado sin dar error. A un generador infinito siempre se le pide con next(), islice, un for con break o una función como hasta().

Pruébalo tú

  • Escribe suma_hasta(limite), que devuelva la suma de los números de Fibonacci que no pasen del límite, usando sum() con hasta() y sin crear ninguna lista.
  • Cambia fibonacci() para que reciba los dos primeros valores (fibonacci(2, 1) da la sucesión de Lucas: 2, 1, 3, 4, 7…).

Programa 2: generador de números primos

Un primo es un número mayor que 1 que solo se divide entre 1 y él mismo. El programa tiene que dar los 15 primeros, cuántos hay hasta 100, el primo número 50, los que hay entre 100 y 150 y las primeras parejas de primos gemelos (dos primos que se diferencian en 2, como 11 y 13). Lo que lo complica: comprobar si un número es primo cuesta trabajo, la secuencia no termina y cada pregunta corta en un sitio distinto.

La idea

Separar las responsabilidades en piezas pequeñas que se enchufan unas a otras como una tubería: es_primo(n) solo decide si un número es primo; primos() es el generador infinito que va probando candidatos; primos_hasta(limite), n_primos(n) y gemelos(gen) son generadores que consumen a primos() y transforman o cortan lo que sale. Cada número viaja por la tubería entera antes de que se calcule el siguiente.

Antes de programar, piensa hasta dónde hace falta probar divisores para saber que 97 es primo. ¿Hace falta llegar a 96?

El código

def es_primo(n):
    """True si n es primo. Solo prueba divisores impares hasta la raíz cuadrada."""
    if n < 2:
        return False
    if n % 2 == 0:
        return n == 2             # el 2 es el único par primo
    i = 3
    while i * i <= n:             # si n tiene un divisor, uno de ellos es <= raíz de n
        if n % i == 0:
            return False
        i += 2
    return True


def primos():
    """Generador infinito de primos."""
    yield 2
    candidato = 3
    while True:
        if es_primo(candidato):
            yield candidato
        candidato += 2            # los pares (salvo el 2) no hace falta mirarlos


def primos_hasta(limite):
    for p in primos():            # consume el generador infinito de uno en uno
        if p > limite:
            return
        yield p


def n_primos(n):
    contador = 0
    for p in primos():
        if contador == n:
            return
        yield p
        contador += 1


def gemelos(gen):
    """Parejas de primos que se diferencian en 2, a partir de un generador de primos."""
    anterior = next(gen)
    for p in gen:                 # el for sigue por donde lo dejó el next()
        if p - anterior == 2:
            yield (anterior, p)
        anterior = p


print('Primeros 15:', list(n_primos(15)))
print('Hasta 100 hay', sum(1 for _ in primos_hasta(100)), 'primos')

g = primos()
for _ in range(49):
    next(g)
print('El primo número 50:', next(g))

print('Entre 100 y 150:', [p for p in primos_hasta(150) if p >= 100])

pares = gemelos(primos())
print('Primeras 5 parejas de gemelos:', [next(pares) for _ in range(5)])

La función es_primo tiene dos atajos que ahorran mucho trabajo:

def es_primo(n):
    """True si n es primo. Solo prueba divisores impares hasta la raíz cuadrada."""
    if n < 2:
        return False
    if n % 2 == 0:
        return n == 2             # el 2 es el único par primo
    i = 3
    while i * i <= n:             # si n tiene un divisor, uno de ellos es <= raíz de n
        if n % i == 0:
            return False
        i += 2
    return True
  • Los pares se resuelven en una línea: return n == 2 da True solo para el 2 y False para el resto.
  • Solo se prueban divisores impares hasta la raíz cuadrada (i * i <= n). Si n = a × b, uno de los dos factores es como mucho √n: si no hay divisor hasta ahí, no lo hay después. Para 97 basta probar 3, 5, 7 y 9 (11 × 11 = 121 ya se pasa): 4 divisiones en lugar de 95.

Y gemelos usa un detalle del protocolo que conviene ver de cerca:

def gemelos(gen):
    """Parejas de primos que se diferencian en 2, a partir de un generador de primos."""
    anterior = next(gen)
    for p in gen:                 # el for sigue por donde lo dejó el next()
        if p - anterior == 2:
            yield (anterior, p)
        anterior = p

Primero hace next(gen) a mano para tener el 2 como anterior. Después, el for p in gen sigue por donde lo dejó ese next(): recibe 3, 5, 7… y no vuelve a empezar, porque gen es un iterador (el mismo objeto), no una lista.

Generadores encadenadosprimos() alimenta a primos_hasta(150), que alimenta al filtro p >= 100; al llegar a 151 primos_hasta hace return y todo se para.primos()infinito: 2, 3, 5, 7…primos_hasta(150)deja pasar hasta 150if p >= 100filtro de la comprensión149149[101, 103, … 149]151 > 150return → StopIterationCada valor recorre la tubería entero antes de que se calcule el siguiente: nunca hay una lista de primos en memoria.
El de dentro produce, el de fuera pide: uno a uno.

En el dibujo, el 149 recorre la tubería entera; el 151 ya no pasa de primos_hasta(150), que hace return y lo detiene todo.

Salida

Primeros 15: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
Hasta 100 hay 25 primos
El primo número 50: 229
Entre 100 y 150: [101, 103, 107, 109, 113, 127, 131, 137, 139, 149]
Primeras 5 parejas de gemelos: [(3, 5), (5, 7), (11, 13), (17, 19), (29, 31)]

Comprobación a mano: hasta 100 hay 25 primos (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97). El primo 50 es 229: los 49 next() del bucle se tragan los 49 primeros y el siguiente es el que imprimimos. Entre 100 y 150 hay 10 (fíjate en que 119 = 7 × 17 no está, y 121 = 11 × 11 tampoco). Las cinco primeras parejas de gemelos: 3 y 5, 5 y 7 (el 5 está en dos parejas), 11 y 13, 17 y 19, 29 y 31.

Vuelta de gemelosanteriorp recibido¿p - anterior == 2?Entrega
(antes del for)2———
123No (1)—
235Sí(3, 5)
357Sí(5, 7)
4711No (4)—
51113Sí(11, 13)

Preguntas que te puedes hacer

¿Por qué sum(1 for _ in primos_hasta(100)) y no len(...)? Porque un generador no tiene len(): no sabe cuántos elementos va a dar hasta que los da. La expresión generadora suma un 1 por cada primo sin crear ninguna lista. len(list(primos_hasta(100))) también vale, pero construye una lista solo para contarla.

¿Por qué n_primos compara contador == n antes del yield? Para no calcular ni un primo de más. Si comprobáramos después del yield, cuando n es 15 el generador calcularía el primo 16 antes de enterarse de que ya ha terminado.

¿Qué pasaría si primos_hasta usara break en vez de return? Lo mismo: después del for no hay nada, así que la función acabaría. Pero si mañana alguien añade código después del bucle, con break se ejecutaría y con return no. return dice exactamente lo que queremos: «este generador ya ha terminado».

¿Por qué [next(pares) for _ in range(5)] y no list(pares)? Porque hay infinitos primos gemelos (o eso se sospecha: es un problema abierto de las matemáticas). list(pares) no terminaría nunca.

Escribir primos_hasta comprobando el límite dentro de primos() con una variable global, o filtrar con [p for p in primos() if p <= 150]. Esa comprensión no termina nunca: el if filtra, pero no para el for. Para cortar un generador infinito hace falta un return, un break o islice.

Pruébalo tú

  • Escribe factores(n), un generador que entregue los factores primos de n usando primos() (para 360: 2, 2, 2, 3, 3, 5).
  • Escribe huecos(gen), que entregue la distancia entre cada primo y el siguiente, y busca el mayor hueco por debajo de 1000.

Programa 3: iterador para la cola de reservas (lista encadenada)

Cuando un libro de la biblioteca está prestado, los socios pueden reservarlo y se ponen en cola: el primero que reservó se lo lleva cuando vuelve. El programa guarda esa cola como una lista encadenada: cada socio va en un nodo que apunta al siguiente. Tiene que poder reservar (añadir al final), atender (sacar al primero), decir cuántos hay, enviar un aviso a cada uno con su puesto y responder a in, list() y str(). Lo que lo complica: la estructura no es una lista de Python, así que el for no sabe recorrerla… hasta que le damos un __iter__.

Las listas encadenadas son el tema siguiente a las pruebas en el temario de FP2. Aquí solo usamos lo imprescindible (un nodo con un dato y un enlace al siguiente); lo importante hoy es cómo se recorre con un generador.

La idea

Cada nodo tiene socio y siguiente. La cola guarda dos referencias: _primero (por donde se atiende) y _ultimo (por donde se reserva, para no tener que recorrer toda la cola para llegar al final). Para recorrerla, __iter__ es un generador que empieza en _primero, entrega el socio de cada nodo y salta al siguiente hasta encontrar None. Como cada llamada a __iter__ crea un generador nuevo, todo lo demás (avisos, __str__, in, list()) sale gratis y se puede repetir.

Antes de programar, piensa qué tiene que pasar con _ultimo cuando atiendes al único socio que quedaba en la cola.

El código

class ColaReservas:
    """Lista encadenada con los socios que esperan un libro, en orden de llegada."""

    class _Nodo:
        def __init__(self, socio):
            self.socio = socio
            self.siguiente = None     # el enlace al nodo de detrás

    def __init__(self, titulo):
        self.titulo = titulo
        self._primero = None
        self._ultimo = None           # guardamos el último para añadir sin recorrer
        self._n = 0

    def reservar(self, socio):
        nodo = self._Nodo(socio)
        if self._primero is None:     # cola vacía: el nuevo es primero y último
            self._primero = nodo
        else:
            self._ultimo.siguiente = nodo
        self._ultimo = nodo
        self._n += 1

    def atender(self):
        """El libro ha vuelto: sale el primero de la cola."""
        if self._primero is None:
            raise IndexError('no hay reservas')
        nodo = self._primero
        self._primero = nodo.siguiente
        if self._primero is None:     # se ha vaciado: tampoco hay último
            self._ultimo = None
        self._n -= 1
        return nodo.socio

    def __len__(self):
        return self._n

    def __iter__(self):               # generador: recorre los nodos sin tocarlos
        nodo = self._primero
        while nodo is not None:
            yield nodo.socio
            nodo = nodo.siguiente

    def avisos(self):
        for puesto, socio in enumerate(self, start=1):
            if puesto == 1:
                yield f'{puesto}. {socio}: te toca'
            else:
                yield f'{puesto}. {socio}: tienes {puesto - 1} delante'

    def __str__(self):
        return ' -> '.join(self) + ' -> None'


cola = ColaReservas('Python a fondo')
for socio in ['Laura', 'Pablo', 'Marta', 'Iván']:
    cola.reservar(socio)

print(cola.titulo, '|', len(cola), 'reservas')
print(cola)
for aviso in cola.avisos():
    print(' ', aviso)
print('¿Marta está?', 'Marta' in cola, '| ¿Sara está?', 'Sara' in cola)
print('Dos recorridos:', list(cola), list(cola))
print('Vuelve el libro: se lo lleva', cola.atender())
print(cola)
print('Tipo de iter(cola):', type(iter(cola)).__name__)

El corazón del programa son estos dos generadores:

def __iter__(self):               # generador: recorre los nodos sin tocarlos
    nodo = self._primero
    while nodo is not None:
        yield nodo.socio
        nodo = nodo.siguiente

def avisos(self):
    for puesto, socio in enumerate(self, start=1):
        if puesto == 1:
            yield f'{puesto}. {socio}: te toca'
        else:
            yield f'{puesto}. {socio}: tienes {puesto - 1} delante'
  • nodo es una variable local del generador, no un atributo de la cola. Cada recorrido tiene la suya, igual que los cursores separados de la teoría, y recorrer no modifica la cola.
  • avisos() usa enumerate(self, start=1): enumerate pide a __iter__ de uno en uno y le pone delante el puesto. Así sale «tienes 2 delante» para Marta sin contar nada a mano.
  • __str__ usa ' -> '.join(self): join acepta cualquier iterable de cadenas, y la cola ya lo es.
La cola de reservas encadenadaCuatro nodos Laura, Pablo, Marta e Iván enlazados hasta None, con _primero y _ultimo, y el cursor nodo del generador en Pablo.ColaReservas('Python a fondo')LaurasocioPablosocioMartasocioIvánsocioNone_primero_ultimonodoEl generador de __iter__ lleva su propia variable nodo: yield nodo.socio y salta al siguiente.Por eso funcionan for, list(), 'Marta' in cola y dos recorridos seguidos sin tocar la lista.
Recorrer no modifica la cola: el cursor es del generador, no de la lista.

En el dibujo, el cursor coral nodo está en Pablo: es el segundo next() del recorrido. La cola no se ha enterado.

Salida

Python a fondo | 4 reservas
Laura -> Pablo -> Marta -> Iván -> None
  1. Laura: te toca
  2. Pablo: tienes 1 delante
  3. Marta: tienes 2 delante
  4. Iván: tienes 3 delante
¿Marta está? True | ¿Sara está? False
Dos recorridos: ['Laura', 'Pablo', 'Marta', 'Iván'] ['Laura', 'Pablo', 'Marta', 'Iván']
Vuelve el libro: se lo lleva Laura
Pablo -> Marta -> Iván -> None
Tipo de iter(cola): generator

Repasemos cada línea. Cuatro reservas en orden de llegada: Laura, Pablo, Marta, Iván. Los avisos numeran desde 1, y el puesto p tiene p - 1 personas delante (Iván, 4 − 1 = 3). 'Marta' in cola da True aunque ColaReservas no tiene __contains__: si no lo encuentra, Python recorre el iterable con __iter__ y compara uno a uno. Los dos list(cola) seguidos salen completos, porque cada uno pidió un generador nuevo. Al volver el libro, atender() saca a Laura y la cola empieza en Pablo.

Paso del generadornodo apunta aEntreganodo pasa a
1Laura'Laura'Pablo
2Pablo'Pablo'Marta
3Marta'Marta'Iván
4Iván'Iván'None
5None(sale del while: StopIteration)—

Preguntas que te puedes hacer

¿Por qué guardar _ultimo si con _primero ya llego a todos? Para que reservar sea inmediato. Sin _ultimo, cada reserva tendría que recorrer la cola hasta el final (con 1000 socios, 1000 saltos por reserva).

¿Por qué atender pone _ultimo = None cuando la cola se vacía? Porque si no, _ultimo seguiría apuntando al nodo que acabamos de sacar, y la siguiente reserva lo enlazaría a ese nodo «fantasma» en lugar de convertirse en la primera.

¿Por qué __iter__ no devuelve self como el iterador de la teoría? Porque la cola es un contenedor, no un recorrido. Si devolviera self con un cursor dentro, print(cola) dentro de un bucle sobre la cola rompería el bucle, como las 3 parejas de la estantería.

¿Puedo atender a alguien mientras recorro la cola con un for? Mejor no. El generador sigue los enlaces en el momento en que pasa por cada nodo; si cambias la estructura a mitad del recorrido, puedes saltarte socios o ver a alguien que ya se fue. Recorre primero (por ejemplo, pendientes = list(cola)) y modifica después.

Escribir __iter__ con return nodo.socio dentro del while. La función deja de ser un generador y el for falla con TypeError: iter() returned non-iterator of type 'str', porque __iter__ tiene que devolver un iterador, no un dato.

Pruébalo tú

  • Añade cancelar(socio), que saque a un socio de cualquier puesto (tendrás que enlazar el nodo de delante con el de detrás).
  • Añade al_reves(), un generador que recorra la cola del último al primero. Pista: como los nodos solo apuntan hacia delante, recorre una vez guardando en una lista y luego entrega del final al principio.

El patrón que debes llevarte

Los tres programas repiten tres formas de generador que vas a reconocer en cualquier ejercicio:

PatrónEsqueletoDónde lo usamos
Generador infinitowhile True: yield valor; avanzarfibonacci(), primos()
Generador que cortafor x in otro: if fin: return; yield xhasta(), primos_hasta(), n_primos()
__iter__ generadornodo = primero; while nodo: yield nodo.dato; nodo = nodo.siguienteColaReservas

Errores típicos en esta práctica

  • Pedir list() a un generador infinito: el programa se queda colgado sin dar error. Corta antes con hasta, islice o un break.
  • Reutilizar el mismo generador para dos preguntas: la segunda empieza donde acabó la primera. Crea uno nuevo para cada recorrido.
  • Escribir la actualización de Fibonacci en dos líneas: anterior = actual seguido de actual = anterior + actual usa el valor ya cambiado. Usa la asignación múltiple.
  • Filtrar un generador infinito con una comprensión: [p for p in primos() if p <= 150] no termina; el if no detiene el for.
  • Guardar el cursor como atributo de un contenedor: el segundo recorrido rompe el primero. El cursor, en una variable local del generador.
  • Olvidar _ultimo al vaciar la cola: la siguiente reserva se pierde enlazada a un nodo que ya no está.

Míralo paso a paso en Python Tutor

En Python Tutor, con Python 3, pega este Fibonacci reducido y avanza paso a paso:

def fibonacci():
    anterior, actual = 0, 1
    while True:
        yield anterior
        anterior, actual = actual, anterior + actual


g = fibonacci()
a = next(g)
b = next(g)
c = next(g)
d = next(g)
e = next(g)
print(a, b, c, d, e)

Salida:

0 1 1 2 3

Qué observar:

  1. Tras g = fibonacci() aparece un objeto generador, pero el marco de fibonacci no se ha creado todavía.
  2. En a = next(g) el marco aparece con anterior = 0 y actual = 1 y se para en el yield: a recibe 0.
  3. En cada next() siguiente, mira cómo cambian a la vez anterior y actual en la línea de la asignación múltiple: (0, 1) → (1, 1) → (1, 2) → (2, 3).
  4. Entre un next() y otro, el marco del generador sigue ahí con sus dos variables: eso es lo que significa «pausado».

Resumen y siguiente paso

  • Un generador infinito solo guarda su estado (dos números en Fibonacci) y entrega valores bajo demanda.
  • Para cortarlo: return en otro generador, islice, next() contados o un for con break; nunca list() sin límite.
  • Los generadores se encadenan como una tubería: cada valor la recorre entera antes de calcular el siguiente.
  • return dentro de un generador lo termina; el for que lo recorre acaba sin error.
  • Un __iter__ escrito como generador hace que un contenedor propio funcione con for, list(), in, join y enumerate, y se pueda recorrer las veces que quieras.
  • Un generador no tiene len(): cuenta con sum(1 for _ in gen).

Checklist: ¿lo has practicado?

  • ¿Has ejecutado los tres programas y comprobado las salidas?
  • ¿Sabes explicar por qué anterior, actual = actual, anterior + actual no necesita variable temporal?
  • ¿Puedes decir tres formas de cortar un generador infinito?
  • ¿Entiendes por qué 'Marta' in cola funciona sin __contains__?
  • ¿Has intentado los «Pruébalo tú» sin mirar la teoría?

Si has respondido que sí a casi todo, estás listo para los ejercicios de iteradores y generadores, donde ya no te doy el código hecho. ¡Nos vemos allí!

Siguiente paso: ejerciciosIteradores y generadores en Python — ejercicios con múltiplos, una secuencia que se reinicia y números perfectos (y una multa que salió letra a letra)

¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Sigue aprendiendo

Deja una respuesta

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