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

URL: https://skillaria.top/es/guides/python/generadores-python

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:
        yield inicio
        inicio -= 1

gen = cuenta_atras(3)
print(gen)          #
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 crea | clase con `__iter__` y `__next__` | función con `yield` o `(x for x in ...)` |

| Estado | a mano, en atributos | se guarda solo, en las variables locales |

| StopIteration | lo lanza usted | aparece solo al salir de la función |

| Extras | lo que escriba | `send()`, `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.

## FAQ

**¿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.


---
Source: Skillaria.Top (https://skillaria.top)
