En esta página

Comprensiones y generadores

14 min lectura TextoCap. 3 — Python intermedio

Comprensiones de lista

Las comprensiones de lista son una forma concisa y Pythónica de crear listas aplicando una expresión a cada elemento de un iterable. La sintaxis básica es:

[expresión for elemento in iterable]

Equivalen a un bucle for que hace append, pero son más legibles y en muchos casos más rápidas:

# Forma tradicional con bucle for
cuadrados_loop = []
for x in range(1, 11):
    cuadrados_loop.append(x ** 2)

# Con comprensión de lista — más concisa
cuadrados = [x ** 2 for x in range(1, 11)]
print(cuadrados)  # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

# Aplicar transformaciones a strings
nombres = ["ana", "bruno", "carmen", "diego"]
capitalizados = [nombre.capitalize() for nombre in nombres]
print(capitalizados)  # ['Ana', 'Bruno', 'Carmen', 'Diego']

# Extraer valores de estructuras anidadas
personas = [
    {"nombre": "Ana", "edad": 25},
    {"nombre": "Bruno", "edad": 30},
    {"nombre": "Carmen", "edad": 28}
]
edades = [p["edad"] for p in personas]
print(edades)  # [25, 30, 28]

# Con llamadas a función
import math
raices = [math.sqrt(x) for x in range(1, 6)]
print(raices)  # [1.0, 1.414..., 1.732..., 2.0, 2.236...]

Comprensiones con condición (filtrado)

Puedes agregar una cláusula if al final para filtrar elementos:

# Solo los pares
pares = [x for x in range(1, 21) if x % 2 == 0]
print(pares)  # [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

# Solo los que cumplen múltiples condiciones
divisibles = [x for x in range(1, 101) if x % 3 == 0 and x % 5 == 0]
print(divisibles)  # [15, 30, 45, 60, 75, 90]

# Filtrar y transformar simultáneamente
productos = [
    {"nombre": "Laptop", "precio": 1299.99, "disponible": True},
    {"nombre": "Mouse", "precio": 29.99, "disponible": False},
    {"nombre": "Teclado", "precio": 89.99, "disponible": True},
    {"nombre": "Monitor", "precio": 399.99, "disponible": True},
]

# Solo productos disponibles y su nombre en mayúsculas
disponibles = [p["nombre"].upper() for p in productos if p["disponible"]]
print(disponibles)  # ['LAPTOP', 'TECLADO', 'MONITOR']

# Filtrar con isinstance()
datos_mixtos = [1, "dos", 3, "cuatro", 5, None, 7]
solo_enteros = [x for x in datos_mixtos if isinstance(x, int)]
print(solo_enteros)  # [1, 3, 5, 7]

Comprensión con if-else (expresión ternaria)

Cuando necesitas transformar elementos de manera diferente según una condición, usas la expresión ternaria dentro de la expresión (no al final):

# Clasificar números como par o impar
clasificados = ["par" if x % 2 == 0 else "impar" for x in range(1, 8)]
print(clasificados)  # ['impar', 'par', 'impar', 'par', 'impar', 'par', 'impar']

# Normalizar calificaciones
notas = [45, 78, 92, 55, 88, 30, 67]
aprobados = ["✓" if nota >= 60 else "✗" for nota in notas]
print(aprobados)  # ['✗', '✓', '✓', '✗', '✓', '✗', '✓']

# Reemplazar None con valor por defecto
valores = [1, None, 3, None, 5]
sin_none = [v if v is not None else 0 for v in valores]
print(sin_none)  # [1, 0, 3, 0, 5]

Comprensiones anidadas

Las comprensiones pueden contener múltiples cláusulas for, lo que equivale a bucles anidados. El orden es el mismo que en los bucles anidados: de exterior a interior.

# Tabla de multiplicar
tabla = [(x, y, x * y) for x in range(1, 4) for y in range(1, 4)]
for a, b, resultado in tabla:
    print(f"{a} × {b} = {resultado}")

# Aplanar una lista de listas
matriz = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
plana = [elemento for fila in matriz for elemento in fila]
print(plana)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Generar combinaciones (producto cartesiano)
colores = ["rojo", "verde"]
tamaños = ["S", "M", "L"]
combinaciones = [(color, tamaño) for color in colores for tamaño in tamaños]
print(combinaciones)
# [('rojo', 'S'), ('rojo', 'M'), ('rojo', 'L'), ('verde', 'S'), ('verde', 'M'), ('verde', 'L')]

# Crear matriz con comprensión anidada
ceros_3x3 = [[0] * 3 for _ in range(3)]
print(ceros_3x3)  # [[0, 0, 0], [0, 0, 0], [0, 0, 0]]

# ¡Trampa! No hagas esto — todas las filas serían el mismo objeto
# ceros_mal = [[0] * 3] * 3  # ¡MAL! Comparten referencia

Comprensiones de diccionario

Similar a las comprensiones de lista, pero crean diccionarios con la sintaxis {clave: valor for ... in ...}:

# Cuadrados de los primeros 5 números
cuadrados_dict = {x: x ** 2 for x in range(1, 6)}
print(cuadrados_dict)  # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# Invertir un diccionario (clave ↔ valor)
original = {"a": 1, "b": 2, "c": 3}
invertido = {v: k for k, v in original.items()}
print(invertido)  # {1: 'a', 2: 'b', 3: 'c'}

# Filtrar entradas de un diccionario
precios = {"manzana": 1.2, "pera": 2.5, "uva": 3.8, "naranja": 1.8}
baratos = {fruta: precio for fruta, precio in precios.items() if precio < 2.0}
print(baratos)  # {'manzana': 1.2, 'naranja': 1.8}

# Transformar valores
precios_con_iva = {producto: round(precio * 1.21, 2)
                  for producto, precio in precios.items()}
print(precios_con_iva)  # {'manzana': 1.45, 'pera': 3.025, ...}

# Desde dos listas
claves = ["nombre", "edad", "ciudad"]
valores = ["María", 29, "Lima"]
perfil = {k: v for k, v in zip(claves, valores)}
print(perfil)  # {'nombre': 'María', 'edad': 29, 'ciudad': 'Lima'}

Comprensiones de conjunto

La misma lógica pero crea sets (elementos únicos):

# Set de cuadrados
cuadrados_set = {x ** 2 for x in range(-5, 6)}
print(cuadrados_set)  # {0, 1, 4, 9, 16, 25} — sin duplicados

# Extraer letras únicas de una frase
frase = "la programación en python es poderosa"
letras_únicas = {letra for letra in frase if letra.isalpha()}
print(sorted(letras_únicas))

# Dominos únicos de una lista de emails
emails = ["[email protected]", "[email protected]", "[email protected]", "[email protected]"]
dominios = {email.split("@")[1] for email in emails}
print(dominios)  # {'gmail.com', 'yahoo.com', 'outlook.com'}

Generadores con yield

Un generador es una función que usa la palabra clave yield para devolver valores uno a la vez. Cuando Python ejecuta yield, la función pausa y guarda su estado completo hasta la próxima llamada:

def contar_hasta(n: int):
    """Generador que cuenta de 1 a n."""
    print("Inicio del generador")
    i = 1
    while i <= n:
        print(f"  Antes de yield {i}")
        yield i
        print(f"  Después de yield {i}")
        i += 1
    print("Fin del generador")

gen = contar_hasta(3)
print(type(gen))  # <class 'generator'>

# Llamar next() manualmente
print(next(gen))  # Inicio del generador / Antes de yield 1 / devuelve 1
print(next(gen))  # Después de yield 1 / Antes de yield 2 / devuelve 2
print(next(gen))  # Después de yield 2 / Antes de yield 3 / devuelve 3

# Próxima llamada lanza StopIteration
try:
    next(gen)
except StopIteration:
    print("Generador exhausto")

Generadores para secuencias infinitas

def enteros_positivos():
    """Genera enteros positivos infinitamente."""
    n = 1
    while True:
        yield n
        n += 1

def primos():
    """Genera números primos infinitamente usando criba."""
    def es_primo(n: int) -> bool:
        if n < 2:
            return False
        return all(n % i != 0 for i in range(2, int(n**0.5) + 1))

    n = 2
    while True:
        if es_primo(n):
            yield n
        n += 1

# Tomar los primeros 10 primos
from itertools import islice
primeros_10_primos = list(islice(primos(), 10))
print(primeros_10_primos)  # [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

yield from — delegar a otro iterable

def cadena(*iterables):
    """Encadena múltiples iterables como un generador."""
    for iterable in iterables:
        yield from iterable  # Delega al iterable

resultado = list(cadena([1, 2], "AB", (3, 4)))
print(resultado)  # [1, 2, 'A', 'B', 3, 4]

Expresiones generadoras

Una expresión generadora es como una comprensión de lista pero con paréntesis. No crea la lista completa en memoria:

# Lista — crea TODO en memoria
lista = [x ** 2 for x in range(1_000_000)]

# Expresión generadora — perezosa, usa O(1) memoria adicional
gen_exp = (x ** 2 for x in range(1_000_000))

# En funciones que aceptan iterables, no necesitas los paréntesis extra
total = sum(x ** 2 for x in range(100))  # Un solo par de paréntesis
print(total)  # 328350

maximo = max(len(p["nombre"]) for p in personas)
print(maximo)

# any() y all() con generadores (cortocircuito eficiente)
tiene_adulto = any(p["edad"] >= 18 for p in personas)
todos_adultos = all(p["edad"] >= 18 for p in personas)

Comparación de rendimiento

import sys
import time

n = 100_000

# Lista — mucha memoria
lista = [x ** 2 for x in range(n)]
print(f"Tamaño lista: {sys.getsizeof(lista):,} bytes")

# Generador — memoria mínima
gen = (x ** 2 for x in range(n))
print(f"Tamaño generador: {sys.getsizeof(gen)} bytes")

# Para suma, el generador es equivalente en velocidad pero usa mucho menos RAM
inicio = time.perf_counter()
suma_lista = sum([x ** 2 for x in range(n)])
t_lista = time.perf_counter() - inicio

inicio = time.perf_counter()
suma_gen = sum(x ** 2 for x in range(n))
t_gen = time.perf_counter() - inicio

print(f"Suma lista: {suma_lista} en {t_lista:.4f}s")
print(f"Suma gen:   {suma_gen} en {t_gen:.4f}s")

Resumen

Las comprensiones de lista, diccionario y conjunto son la forma idiomática de transformar y filtrar datos en Python. Los generadores con yield y las expresiones generadoras son la solución perfecta para procesar grandes cantidades de datos sin cargar todo en memoria. En la próxima lección aprenderemos a manejar errores de manera robusta con try/except.

Comprensiones vs bucles — cuándo usar cada uno
Usa comprensiones para transformaciones simples y concisas. Si la lógica dentro de la comprensión requiere más de una o dos expresiones, es señal de que deberías usar un bucle for regular para mayor legibilidad.
Los generadores ahorran memoria para colecciones grandes
Una comprensión de lista [x**2 for x in range(1_000_000)] crea todos los valores en memoria. Una expresión generadora (x**2 for x in range(1_000_000)) genera cada valor a demanda. Para millones de elementos, la diferencia de memoria es enorme.
Los generadores solo se pueden consumir una vez
Una vez que un generador ha sido exhausto (ya no hay más valores que generar), no puede reiniciarse. Si necesitas iterar múltiples veces, convierte a lista primero con list(generador) o crea un nuevo generador.