Fundamentos

Generadores en Python: yield, iteradores y comprensiones de listas y diccionarios

Actualizado: · Skillaria.Top

Un generador en Python es un objeto que entrega los valores uno a uno, bajo demanda, sin guardar todos en memoria; se crea con una función que usa la palabra clave yield o con una expresión generadora entre paréntesis. Las comprensiones de listas, diccionarios y conjuntos son una forma breve de construir una colección terminada en una sola línea: [x * 2 for x in xs], {k: v for k, v in pares}, {x for x in xs}.

Un generador en Python es un objeto que entrega los valores uno por uno, solo cuando se le piden, y no mantiene toda la secuencia en memoria. Se crea de dos maneras: con una función que usa la palabra clave yield o con una expresión generadora entre paréntesis. Junto a él están las «comprensiones» de listas, diccionarios y conjuntos: una forma breve de construir una colección terminada en una sola línea. Veremos cómo funciona yield, en qué se diferencia un generador de un iterador y cómo evitar que una comprensión se convierta en una línea ilegible.

Iteradores: en qué se basa el bucle for

Para entender los generadores hay que empezar por los iteradores. Un objeto iterable es aquello que se puede recorrer con un bucle: una lista, una cadena, un diccionario, un archivo. Un iterador es un objeto que recuerda la posición actual y, al llamar a next(), entrega el siguiente elemento; cuando se acaban, lanza StopIteration.

numeros = [10, 20, 30]
it = iter(numeros)   # obtener un iterador a partir de la lista
print(next(it))      # 10
print(next(it))      # 20
print(next(it))      # 30
next(it)             # StopIteration

El bucle for hace exactamente esto: llama a iter() y después a next() hasta que aparece StopIteration. Un iterador propio se puede escribir como una clase con los métodos __iter__ y __next__:

class CuentaAtras:
    def __init__(self, inicio):
        self.actual = inicio

    def __iter__(self):
        return self

    def __next__(self):
        if self.actual <= 0:
            raise StopIteration
        valor = self.actual
        self.actual -= 1
        return valor

print(list(CuentaAtras(3)))  # [3, 2, 1]

Funciona, pero tiene mucho código repetitivo. Los generadores hacen lo mismo en tres líneas.

Qué es un generador en Python y cómo funciona yield

Una función generadora es una función normal que contiene yield en su cuerpo. Al llamarla no se ejecuta el código, sino que se devuelve un objeto generador. Cada next() ejecuta el cuerpo hasta el siguiente yield, entrega el valor y congela la función junto con sus variables locales. El siguiente next() continúa exactamente desde ese punto.

def cuenta_atras(inicio):
    while inicio > 0:
        yield inicio
        inicio -= 1

gen = cuenta_atras(3)
print(gen)          # <generator object cuenta_atras at 0x...>
print(next(gen))    # 3
print(list(gen))    # [2, 1] — continúa desde donde se detuvo

Cuando la función llega al final (o a un return), el generador lanza StopIteration por sí mismo, y el bucle for termina sin problemas. De aquí sale la diferencia principal entre yield y return: return termina la función y devuelve un único valor; yield la pausa y puede entregar tantos valores como haga falta.

Generador e iterador: diferencias

Todo generador es un iterador, porque tiene __iter__ y __next__. Pero no todo iterador es un generador. La diferencia está en la forma de crearlo y en lo que ofrece.

Iterador (clase)Generador
Cómo se creaclase con __iter__ y __next__función con yield o (x for x in ...)
Estadoa mano, en atributosse guarda solo, en las variables locales
StopIterationlo lanza ustedaparece solo al salir de la función
Extraslo que escribasend(), throw(), close()

Ambos comparten una propiedad importante: solo se pueden recorrer una vez. Un segundo for sobre un generador agotado no entrega nada; hay que crear uno nuevo.

Generador infinito y lectura de archivos grandes

Un generador no tiene por qué terminar. Una secuencia infinita es segura mientras se tome de ella un número limitado de elementos:

from itertools import islice

def identificadores():
    n = 1
    while True:
        yield f"ID-{n:04d}"
        n += 1

print(list(islice(identificadores(), 3)))  # ['ID-0001', 'ID-0002', 'ID-0003']

El caso más práctico es procesar un archivo grande línea por línea. Los generadores se pueden encadenar en una cadena de procesamiento, y en memoria habrá en cada momento una sola línea, no el archivo entero:

def leer_lineas(ruta):
    with open(ruta, encoding="utf-8") as f:
        for linea in f:
            yield linea.rstrip("\n")

def solo_errores(lineas):
    for linea in lineas:
        if "ERROR" in linea:
            yield linea

for linea in solo_errores(leer_lineas("app.log")):
    print(linea)

yield from y send

yield from transfiere todos los valores de otro objeto iterable, sin necesidad de un bucle anidado:

def aplanar(grupos):
    for g in grupos:
        yield from g

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

El método send() envía un valor al interior del generador: ese valor se convierte en el resultado de la expresión yield. Antes del primer send hay que «arrancar» el generador con next(). Así se escribe, por ejemplo, un acumulador de la media:

def promedio():
    total, cuenta = 0, 0
    media = None
    while True:
        valor = yield media
        total += valor
        cuenta += 1
        media = total / cuenta

p = promedio()
next(p)            # llegar al primer yield
print(p.send(10))  # 10.0
print(p.send(20))  # 15.0

En el código de aplicación, send aparece poco, pero en las entrevistas de trabajo se pregunta a menudo por él.

¿Es range un generador?

No. range es una secuencia perezosa: tampoco guarda todos los números, pero admite indexación, len(), la comprobación in y recorrerla tantas veces como haga falta. Un generador no puede nada de eso.

r = range(5)
print(r[2], len(r), 3 in r)  # 2 5 True
print(list(r), list(r))      # las dos veces [0, 1, 2, 3, 4]

Comprensión de listas: list comprehension

La comprensión de listas (list comprehension) tiene la forma [expresión for x in colección]. Crea una lista nueva y no modifica la original. Se lee de izquierda a derecha: primero «qué poner», después «de dónde sacarlo».

importes = [100, 250, 400]

# bucle normal
con_recargo = []
for importe in importes:
    con_recargo.append(importe * 1.2)

# lo mismo con una comprensión
con_recargo = [importe * 1.2 for importe in importes]
print(con_recargo)  # [120.0, 300.0, 480.0]

A pesar de que se llame «generador de listas», el resultado es una lista terminada, no un generador: todos los elementos se calculan de inmediato.

Comprensión de listas con if e if else

Aquí es donde más se confunden. La condición de filtro va al final y descarta elementos. La condición de elección (el operador ternario) va al principio y convierte cada elemento en uno de dos valores, sin descartar ninguno.

importes = [100, 250, 400]

grandes = [i for i in importes if i > 200]                 # [250, 400]
etiquetas = ["mucho" if i > 200 else "poco" for i in importes]
# ['poco', 'mucho', 'mucho']

# filtro y elección juntos
pares = ["par" if n % 2 == 0 else "impar" for n in range(6) if n > 2]
# ['impar', 'par', 'impar']

La forma [i for i in importes if i > 200 else 0] es un error de sintaxis: else solo puede aparecer en la condición del principio.

Comprensiones anidadas y listas bidimensionales

El doble for se escribe en el mismo orden que los bucles anidados, de arriba abajo. Esto ayuda a no confundir el orden al aplanar una lista de listas:

grupos = [[1, 2], [3], [4, 5]]
plana = [x for g in grupos for x in g]
# equivale a:
# for g in grupos:
#     for x in g:
#         plana.append(x)
print(plana)  # [1, 2, 3, 4, 5]

Una matriz (lista bidimensional) se crea con una comprensión dentro de otra. El error clásico es multiplicar una lista: las filas acaban siendo el mismo objeto.

mal = [[0] * 3] * 2
mal[0][0] = 1
print(mal)   # [[1, 0, 0], [1, 0, 0]] — cambiaron las dos filas

cuadricula = [[0] * 3 for _ in range(2)]
cuadricula[0][0] = 1
print(cuadricula)  # [[1, 0, 0], [0, 0, 0]]

# transposición
m = [[1, 2, 3], [4, 5, 6]]
print([[fila[i] for fila in m] for i in range(3)])  # [[1, 4], [2, 5], [3, 6]]

Comprensión de diccionarios y de conjuntos

La misma técnica sirve para otras colecciones. Las llaves con dos puntos construyen un diccionario (dict comprehension); las llaves sin dos puntos, un conjunto (set comprehension).

gastos = [("comida", 500), ("taxi", 300), ("comida", 200)]

categorias = {cat for cat, _ in gastos}
print(categorias)  # {'comida', 'taxi'} — el orden de un conjunto no está garantizado

ultimo = {cat: importe for cat, importe in gastos}
print(ultimo)      # {'comida': 200, 'taxi': 300}

cuadrados = {n: n ** 2 for n in range(4)}  # {0: 0, 1: 1, 2: 4, 3: 9}
invertido = {v: k for k, v in cuadrados.items()}

Fíjese en ultimo: si una clave se repite, gana el último valor y el importe 500 se pierde. Si hay que sumar, la comprensión no sirve: escriba un bucle o use collections.Counter o defaultdict.

¿Y la comprensión de tuplas?

En Python no existe una «comprensión de tuplas». Los paréntesis dan una expresión generadora, y la tupla se obtiene pasándola a tuple():

t = tuple(n * 2 for n in range(3))  # (0, 2, 4)

Expresión generadora: generar en lugar de guardar

Si cambia los corchetes por paréntesis, se obtiene una expresión generadora. No calcula nada por adelantado y entrega los valores uno a uno. Para un millón de elementos, la diferencia de memoria es enorme:

import sys

como_lista = [n for n in range(1_000_000)]
como_generador = (n for n in range(1_000_000))
print(sys.getsizeof(como_lista))       # unos 8 MB
print(sys.getsizeof(como_generador))   # un par de cientos de bytes

Lo más habitual es usar expresiones generadoras dentro de funciones de agregación. Si el generador es el único argumento, se pueden omitir los paréntesis extra. Además, any() y all() se detienen en cuanto tienen la respuesta y no recorren el resto.

gastos = [("comida", 500), ("taxi", 300), ("comida", 200)]

total = sum(importe for _, importe in gastos)            # 1000
hay_grande = any(i > 400 for _, i in gastos)            # True, no mira más allá de 500
mayor = max(gastos, key=lambda p: p[1])                  # ('comida', 500)

La regla para elegir es sencilla: si el resultado se necesita varias veces, con indexación o con len(), use una lista. Si basta con recorrer una vez y calcular algo, use un generador.

Ámbito de las variables: la variable no se filtra

Una pregunta que hace tropezar incluso a programadores con experiencia:

x = 10
cuadrados = [x for x in range(3)]
print(x)  # 10

La variable del bucle dentro de una comprensión vive en su propio ámbito y no sobrescribe la externa (así ocurre en Python 3; en Python 2 era distinto). En un bucle for normal, después del bucle x valdría 2. Desde la versión 3.12 (PEP 709) las comprensiones se integran en la función que las rodea y funcionan más rápido, pero la regla de visibilidad sigue igual.

Desempaquetado * y ** en comprensiones: novedad de Python 3.15 (PEP 798)

El asterisco para desempaquetar ya se podía usar desde hace tiempo en llamadas a funciones y en literales de lista, pero dentro de una comprensión daba SyntaxError. En Python 3.15 se levantó esa restricción: *elemento significa «inserta aquí todos sus elementos». Funciona en listas, conjuntos y generadores, incluidos los asíncronos.

# Python 3.15+
grupos = [[1, 2], [3], [4, 5]]
plana = [*g for g in grupos]        # [1, 2, 3, 4, 5]
unicos = {*g for g in grupos}       # conjunto
perezoso = (*g for g in grupos)     # generador perezoso

El doble asterisco hace lo mismo con los diccionarios. Es útil para configuraciones por capas («valores por defecto → archivo → opciones»): cada capa siguiente sobrescribe las claves de la anterior.

# Python 3.15+
capas = [{"debug": False, "puerto": 80}, {"puerto": 8080}, {"debug": True}]
config = {**capa for capa in capas}
# {'debug': True, 'puerto': 8080}

Dos advertencias del curso. El asterisco no puede combinarse con una expresión condicional sin paréntesis: [*w if w else 0 for w in semanas] es un error de sintaxis, y la condición debe ir entre paréntesis. Además, este código no se ejecutará en Python 3.14 ni en versiones anteriores: si la biblioteca debe soportar versiones antiguas, mantenga el doble for o use itertools.chain.from_iterable.

Generadores asíncronos

Si una función async def contiene yield, se obtiene un generador asíncrono. Se recorre con async for, y dentro se puede esperar a la red o al disco con await. También existen las comprensiones asíncronas.

import asyncio

async def marcas(n):
    for i in range(n):
        await asyncio.sleep(0.1)
        yield i

async def main():
    resultado = [i async for i in marcas(3)]
    print(resultado)  # [0, 1, 2]

asyncio.run(main())

Errores típicos

  • Comprensión de tres pisos. Dos for, dos if y un ternario en una sola línea no los leerá nadie dentro de una semana. Si no cabe en una línea, escriba un bucle normal: PEP 8 prefiere la legibilidad a la brevedad.
  • Segundo recorrido de un generador. Un segundo sum(gen) devuelve 0, porque el generador ya está agotado.
  • Comprensión por el efecto secundario. [print(x) for x in xs] construye una lista innecesaria de None. Para acciones use un bucle for.
  • Esperar una lista de una expresión generadora. (x for x in xs)[0] da TypeError: los generadores no tienen índices.

Ejercicios de generadores para practicar

  1. Escriba un generador fib() que entregue sin fin los números de Fibonacci y tome los primeros 10 con islice.
  2. Obtenga en una sola línea un diccionario «palabra → longitud» para las palabras de más de tres letras.
  3. Calcule la suma de los cuadrados de los números pares hasta un millón sin crear ninguna lista.
  4. Escriba un generador trozos(secuencia, n) que entregue fragmentos de la lista de n elementos.

Resumen

Un iterador entrega los elementos de uno en uno con next(), y el generador es la forma más sencilla de escribirlo: una función con yield o una expresión entre paréntesis. Las comprensiones de listas, diccionarios y conjuntos construyen una colección terminada en una línea; el filtro if va al final y la elección if else al principio. Si hay que ahorrar memoria o procesar un flujo de datos una sola vez, use un generador; si el resultado se necesita varias veces, una lista. Las comprensiones, los generadores y el nuevo desempaquetado de Python 3.15 se practican en el curso con un registro de gastos.

Preguntas frecuentes

¿Se puede usar return dentro de un generador?

Sí. Un return sin valor simplemente termina el generador; un return con valor guarda ese valor en el atributo value de la excepción StopIteration, y lo recibe la expresión yield from.

¿Cómo convertir un generador en una lista?

Páselo a list(): list(gen). Tenga en cuenta que después el generador queda agotado y todos los valores ocupan memoria.

¿Qué es más rápido: una comprensión de lista o un bucle con append?

Normalmente la comprensión es algo más rápida, porque no llama al método append en cada iteración. Pero conviene elegir según la legibilidad; la diferencia rara vez es decisiva.

¿Cómo saber si un objeto es un generador?

Use inspect.isgenerator(obj) para un objeto o inspect.isgeneratorfunction(f) para una función; para comprobar si un objeto es un iterador, use isinstance(obj, collections.abc.Iterator).

¿En qué se diferencia una expresión generadora de una comprensión de lista?

Los corchetes construyen de inmediato toda la lista en memoria; los paréntesis crean un generador que calcula los elementos bajo demanda y solo se puede recorrer una vez.

Aprende este tema en vídeo

Este tema se explica a fondo y con práctica en el curso «Python 3.15: от новичка до профи».

Ir al curso