En esta página
Decoradores y type hints
Type hints — El sistema de tipos de Python
Los type hints (anotaciones de tipo) son la forma de documentar e indicar los tipos esperados de variables, parámetros y valores de retorno en Python. No afectan la ejecución del programa, pero son invaluables para:
- Detectar errores con herramientas como
mypyopyrightantes de ejecutar - Mejorar el autocompletado en el editor (VS Code, PyCharm)
- Documentar el código de forma más precisa
- Habilitar generación automática de documentación (FastAPI usa type hints para sus esquemas)
Tipos básicos y colecciones
# Tipos primitivos
nombre: str = "Ana"
edad: int = 28
altura: float = 1.68
activo: bool = True
# Anotaciones sin asignación (en clases, esto define campos)
class Usuario:
id: int
nombre: str
email: str
# Colecciones modernas (Python 3.9+)
# Antes: from typing import List, Dict, Tuple, Set
# Ahora: usa directamente list[], dict[], tuple[], set[]
numeros: list[int] = [1, 2, 3, 4, 5]
configuracion: dict[str, str] = {"host": "localhost", "puerto": "8080"}
coordenadas: tuple[float, float] = (40.4168, -3.7038)
etiquetas: set[str] = {"python", "backend", "api"}
# Tipos opcionales (Python 3.10+)
# Antes: from typing import Optional; Optional[str] → str | None
resultado: str | None = None
nombre_usuario: str | None = "ana_garcia"
# Uniones (Python 3.10+)
# Antes: from typing import Union; Union[int, str]
identificador: int | str = 42
valor: int | float | str = 3.14El módulo typing — Tipos avanzados
from typing import (
Callable,
TypeVar,
Generic,
Protocol,
TypedDict,
Literal,
Final,
ClassVar,
Annotated,
Any,
Never,
Self,
)
# Callable — para funciones como argumentos
def aplicar_transformacion(
datos: list[int],
transformacion: Callable[[int], int]
) -> list[int]:
return [transformacion(x) for x in datos]
resultado = aplicar_transformacion([1, 2, 3], lambda x: x ** 2)
# Callable con tipos de retorno complejos
ProcesadorTexto = Callable[[str, bool], str]
# Literal — solo acepta valores específicos
def configurar_modo(modo: Literal["debug", "producción", "prueba"]) -> None:
print(f"Modo configurado: {modo}")
configurar_modo("debug") # OK
# configurar_modo("otro") # mypy lanza error
# Final — constantes que no pueden reasignarse
MAX_REINTENTOS: Final = 3
VERSION: Final[str] = "1.0.0"
# ClassVar — atributos de clase (no de instancia)
class Singleton:
_instancia: ClassVar["Singleton | None"] = None
@classmethod
def obtener_instancia(cls) -> "Singleton":
if cls._instancia is None:
cls._instancia = cls()
return cls._instancia
# TypedDict — diccionarios con estructura específica
class ConfigDB(TypedDict):
host: str
puerto: int
nombre: str
usuario: str
contraseña: str
def conectar(config: ConfigDB) -> None:
print(f"Conectando a {config['host']}:{config['puerto']}/{config['nombre']}")
config: ConfigDB = {
"host": "localhost",
"puerto": 5432,
"nombre": "mi_db",
"usuario": "admin",
"contraseña": "secreto"
}TypeVar y Genéricos
from typing import TypeVar, Generic
T = TypeVar("T")
K = TypeVar("K")
V = TypeVar("V")
# Función genérica
def primero(lista: list[T]) -> T | None:
return lista[0] if lista else None
print(primero([1, 2, 3])) # 1 (inferido como int)
print(primero(["a", "b"])) # 'a' (inferido como str)
print(primero([])) # None
# Clase genérica
class Pila(Generic[T]):
"""Estructura de datos pila (LIFO) genérica."""
def __init__(self) -> None:
self._items: list[T] = []
def apilar(self, item: T) -> None:
self._items.append(item)
def desapilar(self) -> T:
if not self._items:
raise IndexError("La pila está vacía")
return self._items.pop()
def ver_tope(self) -> T | None:
return self._items[-1] if self._items else None
def __len__(self) -> int:
return len(self._items)
def __bool__(self) -> bool:
return bool(self._items)
pila_int: Pila[int] = Pila()
pila_int.apilar(1)
pila_int.apilar(2)
pila_int.apilar(3)
print(pila_int.desapilar()) # 3
pila_str: Pila[str] = Pila()
pila_str.apilar("Python")
pila_str.apilar("FastAPI")Anotaciones de retorno especiales
from typing import Never, NoReturn
def lanzar_error(mensaje: str) -> Never:
"""Función que SIEMPRE lanza una excepción."""
raise ValueError(mensaje)
def salir_programa(codigo: int = 0) -> NoReturn:
"""Función que nunca retorna normalmente."""
import sys
sys.exit(codigo)
# Self — para métodos que devuelven la instancia (Python 3.11+)
from typing import Self
class Constructor:
def __init__(self) -> None:
self._datos: dict[str, str] = {}
def agregar(self, clave: str, valor: str) -> Self:
self._datos[clave] = valor
return self # Permite encadenamiento
def construir(self) -> dict[str, str]:
return self._datos.copy()
resultado = (Constructor()
.agregar("nombre", "Ana")
.agregar("rol", "admin")
.construir())Decoradores en Python
Un decorador es una función que recibe otra función y devuelve una versión modificada de ella. Es un patrón de diseño poderoso para agregar comportamiento transversal (logging, caché, validación, autenticación) sin modificar el código original.
Decorador básico
import time
import functools
from typing import Callable, TypeVar, Any
F = TypeVar("F", bound=Callable[..., Any])
def cronometrar(func: F) -> F:
"""Mide y muestra el tiempo de ejecución de una función."""
@functools.wraps(func)
def envoltura(*args: Any, **kwargs: Any) -> Any:
inicio = time.perf_counter()
try:
resultado = func(*args, **kwargs)
return resultado
finally:
duracion = time.perf_counter() - inicio
print(f"[Tiempo] {func.__name__}: {duracion:.4f}s")
return envoltura # type: ignore[return-value]
@cronometrar
def ordenar_grande(n: int) -> list[int]:
"""Genera y ordena una lista grande de números."""
import random
datos = [random.randint(0, n) for _ in range(n)]
return sorted(datos)
resultado = ordenar_grande(100_000)
print(f"Primer elemento: {resultado[0]}")
# [Tiempo] ordenar_grande: 0.0523sDecorador con argumentos
Para crear un decorador que acepte argumentos, necesitas un nivel extra de función:
import logging
from functools import wraps
from typing import Callable, TypeVar, Any
F = TypeVar("F", bound=Callable[..., Any])
def registrar(nivel: str = "INFO", incluir_args: bool = False):
"""Decorador configurable que registra llamadas a funciones."""
nivel_log = getattr(logging, nivel.upper(), logging.INFO)
def decorador(func: F) -> F:
@wraps(func)
def envoltura(*args: Any, **kwargs: Any) -> Any:
if incluir_args:
logging.log(nivel_log, f"Llamando {func.__name__}({args}, {kwargs})")
else:
logging.log(nivel_log, f"Llamando {func.__name__}")
try:
resultado = func(*args, **kwargs)
logging.log(nivel_log, f"{func.__name__} completó exitosamente")
return resultado
except Exception as e:
logging.error(f"{func.__name__} lanzó {type(e).__name__}: {e}")
raise
return envoltura # type: ignore[return-value]
return decorador
@registrar(nivel="DEBUG", incluir_args=True)
def calcular_potencia(base: float, exponente: float) -> float:
return base ** exponente
@registrar(nivel="INFO")
def procesar_pagos(mes: str) -> int:
return 42 # Número de pagos procesadosCaché con decoradores
import functools
from typing import Callable, TypeVar, Any
# lru_cache — memoización integrada
from functools import lru_cache, cache
@cache # Equivalente a @lru_cache(maxsize=None)
def fibonacci(n: int) -> int:
"""Fibonacci con memoización automática."""
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(50)) # Muy rápido gracias a la caché
print(fibonacci.cache_info()) # CacheInfo(hits=48, misses=51, ...)
@lru_cache(maxsize=128) # Caché con límite de 128 entradas
def buscar_usuario_db(user_id: int) -> dict:
"""Simula búsqueda en base de datos con caché."""
print(f"Consultando BD para usuario {user_id}...")
return {"id": user_id, "nombre": f"Usuario {user_id}"}
# Decorador de caché personalizado con TTL
def caché_con_ttl(segundos: float):
"""Caché que expira después de N segundos."""
def decorador(func: F) -> F:
cache_interna: dict[tuple, tuple] = {}
@wraps(func)
def envoltura(*args: Any, **kwargs: Any) -> Any:
clave = (args, tuple(sorted(kwargs.items())))
ahora = time.time()
if clave in cache_interna:
valor, timestamp = cache_interna[clave]
if ahora - timestamp < segundos:
return valor
resultado = func(*args, **kwargs)
cache_interna[clave] = (resultado, ahora)
return resultado
return envoltura # type: ignore[return-value]
return decorador
@caché_con_ttl(segundos=60)
def obtener_tasa_cambio(moneda: str) -> float:
"""Caché de 60 segundos para tasas de cambio."""
print(f"Consultando tasa de {moneda}...")
return 1.08 if moneda == "EUR" else 0.79 # SimuladoDecoradores apilados
Los decoradores se aplican de abajo hacia arriba:
@registrar(nivel="INFO")
@cronometrar
@caché_con_ttl(segundos=30)
def proceso_costoso(datos: list[int]) -> int:
"""Función con múltiples decoradores apilados."""
time.sleep(0.1) # Simula trabajo pesado
return sum(datos)
# Equivalente a:
# proceso_costoso = registrar(nivel="INFO")(cronometrar(caché_con_ttl(30)(proceso_costoso)))
resultado = proceso_costoso([1, 2, 3, 4, 5])Decoradores de clase
from typing import TypeVar, Type
C = TypeVar("C", bound=type)
def singleton(cls: C) -> C:
"""Decorador que convierte una clase en singleton."""
instancias: dict[C, C] = {}
@wraps(cls)
def obtener_instancia(*args: Any, **kwargs: Any) -> C:
if cls not in instancias:
instancias[cls] = cls(*args, **kwargs) # type: ignore
return instancias[cls]
return obtener_instancia # type: ignore[return-value]
@singleton
class ConexiónBD:
def __init__(self, url: str = "sqlite:///app.db") -> None:
self.url = url
print(f"Conectando a {url}")
bd1 = ConexiónBD()
bd2 = ConexiónBD()
print(bd1 is bd2) # True — es el mismo objetoVerificación de tipos con mypy
# Instalar
pip install mypy
# Verificar un archivo
mypy mi_archivo.py
# Verificación estricta
mypy --strict mi_archivo.py
# Verificar un proyecto completo
mypy src/
# Configurar en pyproject.toml
# [tool.mypy]
# strict = true
# python_version = "3.14"Resumen
Los type hints transforman Python en un lenguaje con tipado opcional pero poderoso. Los TypeVar y Generic permiten escribir código reutilizable con seguridad de tipos. Los decoradores son una forma elegante de agregar comportamiento transversal sin modificar funciones existentes. @functools.wraps es obligatorio para preservar la identidad de la función decorada. En la próxima y última lección pondrás todo en práctica construyendo un proyecto completo.
Inicia sesión para guardar tu progreso