Comprensiones y Generadores
Escribe código elegante y eficiente con comprensiones de lista/dict/set, funciones generadoras, yield y expresiones generadoras
Comprensiones y Generadores
Las comprensiones proporcionan una sintaxis concisa para crear colecciones. Los generadores permiten evaluación perezosa, procesando datos un elemento a la vez en lugar de cargar todo en memoria.
Comprensiones de Lista
Sintaxis básica: [expression for item in iterable if condition]
# Enfoque tradicional
squares = []
for x in range(10):
squares.append(x ** 2)
# Comprensión de lista
squares = [x ** 2 for x in range(10)]
# Con condición
evens = [x for x in range(20) if x % 2 == 0]
# Bucles anidados
pairs = [(x, y) for x in range(3) for y in range(3)]
# Transformación
words = ["hello", "world", "python"]
upper_words = [w.upper() for w in words]# Transformaciones complejas
values = [1, -2, 3, -4, 5, -6]
processed = [x * 2 if x > 0 else abs(x) * 10 for x in values]
print(processed) # [2, 20, 6, 40, 10, 60]
# Aplanar una matriz
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [num for row in matrix for num in row]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Producto cartesiano
colors = ["red", "blue"]
sizes = ["S", "M", "L"]
inventory = [(c, s) for c in colors for s in sizes]
print(inventory)Comprensiones de Diccionario
# Diccionario de cuadrados: {0: 0, 1: 1, 2: 4, 3: 9, ...}
squares = {x: x ** 2 for x in range(10)}
# Filtrando y transformando
words = ["apple", "banana", "cherry", "date"]
word_lengths = {w: len(w) for w in words if len(w) > 4}
print(word_lengths) # {"apple": 5, "banana": 6, "cherry": 6}
# Intercambiando claves y valores
original = {"a": 1, "b": 2, "c": 3}
swapped = {v: k for k, v in original.items()}
print(swapped) # {1: "a", 2: "b", 3: "c"}
# Patrón enumerate
indexed = {i: char for i, char in enumerate("hello")}
print(indexed) # {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}Comprensiones de Conjunto
# Cuadrados pares únicos
even_squares = {x ** 2 for x in range(20) if x % 2 == 0}
print(even_squares) # {0, 4, 16, 36, 64, 100, 144, 196, 256, 324}
# Encontrar caracteres únicos
text = "hello world"
unique_chars = {c for c in text if c != " "}
print(unique_chars) # {"h", "e", "l", "o", "w", "r", "d"}Funciones Generadoras con yield
Las funciones generadoras producen valores perezosamente usando yield:
def count_up_to(n: int):
i = 0
while i < n:
yield i
i += 1
# Los generadores son perezosos — nada se computa aún
counter = count_up_to(5)
# Valores producidos bajo demanda
print(next(counter)) # 0
print(next(counter)) # 1
print(list(counter)) # [2, 3, 4] (restantes)
# O iterar directamente
for num in count_up_to(3):
print(num) # 0, 1, 2Generadores Infinitos
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
print([next(fib) for _ in range(10)]) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
def counter(start: int = 0, step: int = 1):
while True:
yield start
start += step
c = counter(10, 5)
print([next(c) for _ in range(4)]) # [10, 15, 20, 25]Expresiones Generadoras
Similares a las comprensiones de lista pero con paréntesis — perezosas y eficientes en memoria:
# Comprensión de lista — crea lista completa en memoria
squares_list = [x ** 2 for x in range(1000000)]
# Expresión generadora — perezosa, un elemento a la vez
squares_gen = (x ** 2 for x in range(1000000))
# Suma del primer millón de cuadrados (no se necesita lista grande)
total = sum(x ** 2 for x in range(1000000))Las expresiones generadoras son de un solo uso. Una vez agotadas, no se pueden re-iterar. Envuélvelas en list() si necesitas múltiples pasadas.
gen = (x * 2 for x in range(5))
print(list(gen)) # [0, 2, 4, 6, 8]
print(list(gen)) # [] — ¡agotado!Encadenamiento y Pipeline de Generadores
def numbers():
for i in range(10):
yield i
def even(iterable):
for x in iterable:
if x % 2 == 0:
yield x
def squared(iterable):
for x in iterable:
yield x ** 2
# Pipeline — cada función procesa un elemento a la vez
pipeline = squared(even(numbers()))
print(list(pipeline)) # [0, 4, 16, 36, 64]
# Lo mismo con expresiones generadoras
result = (x ** 2 for x in range(10) if x % 2 == 0)
print(list(result)) # [0, 4, 16, 36, 64]yield from — Delegando a Subgeneradores
def chain(*iterables):
for iterable in iterables:
yield from iterable
combined = chain([1, 2, 3], "abc", range(4, 6))
print(list(combined)) # [1, 2, 3, "a", "b", "c", 4, 5]
# Aplanar listas anidadas (recursivo)
def flatten(nested):
for item in nested:
if isinstance(item, (list, tuple)):
yield from flatten(item)
else:
yield item
deep = [1, [2, [3, 4], 5], 6]
print(list(flatten(deep))) # [1, 2, 3, 4, 5, 6]Comparación de Memoria
import sys
# Comprensión de lista — todos los valores en memoria
list_comp = [x ** 2 for x in range(100000)]
print(f"List size: {sys.getsizeof(list_comp)} bytes")
# Expresión generadora — memoria mínima
gen_exp = (x ** 2 for x in range(100000))
print(f"Generator size: {sys.getsizeof(gen_exp)} bytes")
# Función generadora — también mínima
def gen_func():
for x in range(100000):
yield x ** 2
print(f"Gen function size: {sys.getsizeof(gen_func())} bytes")Una expresión generadora típicamente ocupa ~120 bytes independientemente de cuántos elementos produzca. Una comprensión de lista crece proporcionalmente al número de elementos.
Mundo Real: Procesamiento Perezoso de Archivos
from pathlib import Path
def read_lines(paths: list[Path]):
"""Produce líneas perezosamente desde múltiples archivos."""
for path in paths:
with open(path, "r", encoding="utf-8") as f:
yield from f
def filter_lines(lines, keyword: str):
"""Filtra líneas perezosamente que contienen la palabra clave."""
for line in lines:
if keyword in line:
yield line
def count_words(lines):
"""Cuenta palabras entre líneas (perezoso)."""
for line in lines:
yield len(line.split())
# Procesar múltiples archivos de registro sin cargar todo
log_dir = Path("/var/log")
log_files = list(log_dir.glob("*.log"))
lines = read_lines(log_files[:5]) # ¡Aún sin lectura!
error_lines = filter_lines(lines, "ERROR") # ¡Aún perezoso!
word_counts = count_words(error_lines) # ¡Aún perezoso!
# Solo ahora ocurre la ejecución
total = sum(word_counts)
print(f"Total words in ERROR lines: {total}")Mundo Real: Paginación de API en Streaming
from typing import Generator
import requests
def paginate(url: str, page_size: int = 100) -> Generator[dict, None, None]:
"""Produce elementos perezosamente desde una API paginada."""
page = 1
while True:
response = requests.get(url, params={"page": page, "size": page_size})
data = response.json()
if not data["items"]:
break
yield from data["items"]
page += 1
# Procesar todos los usuarios sin cargar todas las páginas en memoria
for user in paginate("https://api.example.com/users"):
if user["status"] == "active":
print(f"Processing {user['email']}")Preguntas de Práctica
- Escribe una comprensión de lista que produzca cuadrados de los números 1-10, pero solo para números impares.
- ¿Cuál es la diferencia entre una función generadora (usando
yield) y una función normal? - Reescribe este bucle como una comprensión de diccionario:
result = {}; for k, v in items: if len(v) > 3: result[k] = v.upper() - ¿Por qué una expresión generadora
(x for x in range(1_000_000))es más eficiente en memoria que una comprensión de lista? - ¿Qué sucede cuando llamas a
next()en un generador que no tiene más elementos que producir? - Escribe una función generadora que produzca los primeros
nnúmeros primos. - ¿Cómo difiere
yield fromde iterar manualmente y producir cada elemento? - Crea un pipeline que lea un archivo CSV, filtre filas donde valor > 50, y eleve al cuadrado el resultado — todo perezosamente.
- ¿Qué hace el método
send()en un generador? ¿En qué se diferencia denext()? - ¿Cuándo deberías usar un generador en lugar de una lista? ¿Cuándo no?