# Referencia Completa — Patrones Python

## Generators e Iteradores

```python
from typing import Iterator, Generator

# Generator simple: procesar archivos grandes sin cargar todo en memoria
def leer_csv_grande(ruta: str) -> Iterator[dict]:
    with open(ruta, encoding="utf-8") as f:
        encabezados = next(f).strip().split(",")
        for linea in f:
            valores = linea.strip().split(",")
            yield dict(zip(encabezados, valores))

# Generator con envío de valores (send)
def acumulador() -> Generator[float, float, str]:
    total = 0.0
    while True:
        valor = yield total
        if valor is None:
            return f"Total final: {total}"
        total += valor

# Generators para pipelines de datos
def filtrar_activos(registros: Iterator[dict]) -> Iterator[dict]:
    return (r for r in registros if r.get("activo") == "true")

def normalizar_nombres(registros: Iterator[dict]) -> Iterator[dict]:
    for r in registros:
        r["nombre"] = r["nombre"].strip().title()
        yield r

# Composición de pipeline
def pipeline_etl(ruta: str) -> Iterator[dict]:
    registros = leer_csv_grande(ruta)
    registros = filtrar_activos(registros)
    registros = normalizar_nombres(registros)
    return registros
```

---

## Decoradores

```python
import functools
import logging
from typing import Callable

logger = logging.getLogger(__name__)

# Decorator para reintentar en caso de error
def reintentar(max_intentos: int = 3, excepciones: tuple = (Exception,)):
    def decorador(fn: Callable) -> Callable:
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            for intento in range(1, max_intentos + 1):
                try:
                    return fn(*args, **kwargs)
                except excepciones as e:
                    if intento == max_intentos:
                        raise
                    logger.warning(f"Intento {intento}/{max_intentos} falló: {e}")
            return None  # nunca alcanzado, satisface mypy
        return wrapper
    return decorador

@reintentar(max_intentos=3, excepciones=(httpx.TimeoutException,))
def obtener_datos_externos(url: str) -> dict:
    return httpx.get(url, timeout=5).json()

# Decorator de caché simple con functools.lru_cache
from functools import lru_cache

@lru_cache(maxsize=128)
def factorial(n: int) -> int:
    return 1 if n <= 1 else n * factorial(n - 1)

# Decorator que preserva tipo de retorno (typed decorator)
def validar_entrada(fn: Callable[P, T]) -> Callable[P, T]:
    @functools.wraps(fn)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        for arg in args:
            if arg is None:
                raise ValueError("Los argumentos no pueden ser None")
        return fn(*args, **kwargs)
    return wrapper
```

---

## Async/await

```python
import asyncio
from typing import Any

# Función async básica
async def obtener_usuario(usuario_id: str) -> dict:
    async with httpx.AsyncClient() as client:
        respuesta = await client.get(f"/usuarios/{usuario_id}")
        respuesta.raise_for_status()
        return respuesta.json()

# Ejecución concurrente con gather
async def obtener_multiples(ids: list[str]) -> list[dict]:
    tareas = [obtener_usuario(uid) for uid in ids]
    return await asyncio.gather(*tareas)

# Ejecución con timeout
async def obtener_con_timeout(usuario_id: str) -> dict | None:
    try:
        return await asyncio.wait_for(obtener_usuario(usuario_id), timeout=5.0)
    except asyncio.TimeoutError:
        logger.error(f"Timeout obteniendo usuario {usuario_id}")
        return None

# Semáforo para limitar concurrencia
semaforo = asyncio.Semaphore(10)

async def descarga_controlada(url: str) -> bytes:
    async with semaforo:
        async with httpx.AsyncClient() as client:
            respuesta = await client.get(url)
            return respuesta.content
```

---

## Manejo de Errores Idiomático

```python
# Jerarquía de excepciones del proyecto
class ErrorAplicacion(Exception):
    """Base para todos los errores de la aplicación."""

class ErrorValidacion(ErrorAplicacion):
    def __init__(self, campo: str, mensaje: str):
        self.campo = campo
        super().__init__(f"Validación fallida en '{campo}': {mensaje}")

class ErrorNoEncontrado(ErrorAplicacion):
    def __init__(self, entidad: str, id: str):
        super().__init__(f"{entidad} con id '{id}' no encontrado")

# Encadenar excepciones preserva el traceback original
def cargar_configuracion(ruta: str) -> dict:
    try:
        with open(ruta) as f:
            return json.load(f)
    except FileNotFoundError as e:
        raise ErrorAplicacion(f"Archivo de config no encontrado: {ruta}") from e
    except json.JSONDecodeError as e:
        raise ErrorAplicacion(f"JSON inválido en: {ruta}") from e
```

---

## Anti-patrones Python Completos

```python
# MAL: argumento mutable como default
def agregar(item, lista=[]):   # La lista persiste entre llamadas
    lista.append(item)
    return lista

# BIEN: usar None como centinela
def agregar(item, lista=None):
    if lista is None:
        lista = []
    lista.append(item)
    return lista

# MAL: comparar tipo con type()
if type(x) == list:
    ...

# BIEN: isinstance maneja herencia
if isinstance(x, list):
    ...

# MAL: bare except
try:
    operacion_riesgosa()
except:
    pass  # silencia TODO: incluyendo KeyboardInterrupt y SystemExit

# BIEN: excepción específica
try:
    operacion_riesgosa()
except ValueError as e:
    logger.error(f"Valor inválido: {e}")

# MAL: concatenación en loop (O(n²))
resultado = ""
for parte in partes:
    resultado += parte

# BIEN: join (O(n))
resultado = "".join(partes)
```
