CAP 12 · LEC 01·Python avanzado

Decoradores: funciones que modifican funciones

Un decorador es una función que recibe una función y retorna una función. Con @, Python aplica el decorador automáticamente al definir la función. Son la base de frameworks como Flask, FastAPI y Django.

● AVANZADO12 min lectura4 ejerciciospor Fernando Herrera · actualizado mayo de 2026
¿Encontraste un error o algo que mejorar?Editá esta lección en GitHub →

Funciones como valores (prerequisito)

Para entender decoradores hay que entender primero que en Python las funciones son objetos de primera clase: se pueden asignar a variables, pasar como argumentos y retornar desde otras funciones.

# Las funciones son objetos — se pueden asignar def greet(name: str) -> str: return f"Hola, {name}" say_hello = greet # sin paréntesis: referencia a la función print(say_hello("Ana")) # Hola, Ana print(greet is say_hello) # True — misma función # Se pueden pasar como argumentos def apply(func, value): return func(value) result = apply(str.upper, "python") print(result) # PYTHON # Se pueden retornar desde funciones (closure) def make_multiplier(factor: int): def multiplier(x: int) -> int: return x * factor # captura 'factor' del scope exterior return multiplier # retorna la función, no el resultado triple = make_multiplier(3) double = make_multiplier(2) print(triple(5)) # 15 print(double(5)) # 10 # Función anidada captura el scope (closure) def counter(): count = 0 def increment(): nonlocal count count += 1 return count return increment c = counter() print(c(), c(), c()) # 1 2 3
SalidaHola, Ana True PYTHON 15 10 1 2 3

Decorador básico manual

Un decorador es simplemente una función que recibe una función y retorna otra función (el "wrapper"). El wrapper puede ejecutar código antes y después de llamar a la función original.

# Un decorador básico: función que envuelve a otra función def loud(func): def wrapper(*args, **kwargs): print(f"→ Llamando a {func.__name__}") result = func(*args, **kwargs) print(f"← {func.__name__} retornó: {result}") return result return wrapper def add(a: int, b: int) -> int: return a + b # Aplicar el decorador manualmente add_loud = loud(add) result = add_loud(3, 4) # → Llamando a add # ← add retornó: 7 # La función original no se modifica print(add(3, 4)) # 7 — add sigue funcionando igual # add_loud es una nueva función que envuelve a add print(add_loud.__name__) # wrapper — problema: se perdió el nombre # (lo solucionamos con functools.wraps en la siguiente sección)
Salida→ Llamando a add ← add retornó: 7 7 wrapper

La sintaxis @ (syntactic sugar)

@decorador antes de una función es azúcar sintáctica para funcion = decorador(funcion). Python aplica el decorador en el momento de la definición, no en la llamada.

# Equivalencia exacta: # @loud # def add(a, b): ... # # es lo mismo que: # def add(a, b): ... # add = loud(add) def loud(func): def wrapper(*args, **kwargs): print(f"→ {func.__name__}({args}, {kwargs})") result = func(*args, **kwargs) print(f"← resultado: {result}") return result return wrapper # Con la sintaxis @ @loud def multiply(a: int, b: int) -> int: """Multiplica dos números.""" return a * b # Se aplica al definir la función, no al llamarla result = multiply(4, 5) # → multiply((4, 5), {}) # ← resultado: 20 # Múltiples decoradores: se aplican de abajo hacia arriba def uppercase_result(func): def wrapper(*args, **kwargs): result = func(*args, **kwargs) return str(result).upper() return wrapper def add_exclamation(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) + "!" return wrapper @add_exclamation # se aplica segundo (más exterior) @uppercase_result # se aplica primero (más interior) def greet(name: str) -> str: return f"hola {name}" print(greet("python")) # HOLA PYTHON!
Salida→ multiply((4, 5), {}) ← resultado: 20 HOLA PYTHON!

functools.wraps — preservar metadatos

Sin functools.wraps, el decorador "roba" el nombre y docstring de la función original. @wraps(func) copia los metadatos al wrapper, preservando __name__, __doc__, __annotations__, etc.

import functools # Sin @wraps: se pierden los metadatos def decorator_sin_wraps(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @decorator_sin_wraps def mi_funcion(): """Esta es la documentación.""" pass print(mi_funcion.__name__) # wrapper — ¡incorrecto! print(mi_funcion.__doc__) # None — ¡perdida! # Con @wraps: metadatos preservados def decorator_con_wraps(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @decorator_con_wraps def mi_funcion_bien(): """Esta es la documentación.""" pass print(mi_funcion_bien.__name__) # mi_funcion_bien — correcto print(mi_funcion_bien.__doc__) # Esta es la documentación. — correcto print(mi_funcion_bien.__wrapped__) # <function mi_funcion_bien...> — referencia al original # @wraps también preserva __annotations__ def log_types(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @log_types def divide(a: float, b: float) -> float: return a / b print(divide.__annotations__) # {'a': <class 'float'>, 'b': <class 'float'>, 'return': <class 'float'>}
Salidawrapper None mi_funcion_bien Esta es la documentación. <function mi_funcion_bien at 0x...> {'a': <class 'float'>, 'b': <class 'float'>, 'return': <class 'float'>}
Regla de oro

Siempre incluye @functools.wraps(func) en el wrapper interior de cualquier decorador. Sin él, las herramientas de introspección, pytest, Sphinx y los debuggers verán el wrapper en vez de la función original.

Casos reales: logging, timing, auth

Los decoradores brillan en casos de corte transversal: funcionalidad que se repite en muchas funciones pero que no pertenece a su lógica central.

import functools import time import logging logging.basicConfig(level=logging.INFO, format="%(message)s") # Decorador de timing: mide cuánto tarda la función def timer(func): @functools.wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - start logging.info(f"{func.__name__} tardó {elapsed:.4f}s") return result return wrapper # Decorador de logging: registra entrada y salida def log_call(func): @functools.wraps(func) def wrapper(*args, **kwargs): logging.info(f"Llamando {func.__name__} con args={args} kwargs={kwargs}") result = func(*args, **kwargs) logging.info(f"{func.__name__} retornó {result!r}") return result return wrapper # Decorador de autenticación simulada def require_auth(func): @functools.wraps(func) def wrapper(*args, **kwargs): # En producción consultaría el token del request user = kwargs.get("user") if not user: raise PermissionError("Autenticación requerida") return func(*args, **kwargs) return wrapper # Uso combinado @timer @log_call def process_data(items: list) -> int: """Procesa una lista y retorna la suma.""" import time time.sleep(0.01) # simula trabajo return sum(items) result = process_data([1, 2, 3, 4, 5]) print(result) # 15 @require_auth def get_secret(user: str = None) -> str: return f"Secreto para {user}" try: get_secret() # sin user except PermissionError as e: print(e) # Autenticación requerida print(get_secret(user="Ana")) # Secreto para Ana
SalidaINFO:Llamando process_data con args=([1, 2, 3, 4, 5],) kwargs={} INFO:process_data retornó 15 INFO:process_data tardó 0.0102s 15 Autenticación requerida Secreto para Ana

Practica