Decoradores en Python: qué son, cómo funcionan y cómo escribir uno propio
Un decorador en Python es una función que recibe otra función y devuelve una nueva, normalmente un envoltorio que añade comportamiento antes o después de la llamada: registro, medición del tiempo, caché, comprobación de permisos. La línea @decorador encima de def equivale a func = decorador(func); para que el envoltorio conserve el nombre y el docstring de la función original se usa functools.wraps.
Un decorador en Python es una función que recibe otra función y devuelve una nueva, normalmente un envoltorio alrededor de la original. Así se añade comportamiento a una función —registro de eventos, medición del tiempo, caché, reintentos— sin cambiar su código. Los decoradores se escriben con la línea @nombre encima de la declaración de la función y aparecen en todas partes: en Flask y FastAPI, en pytest y en la biblioteca estándar.
Qué es un decorador, explicado sencillamente
Imagine un regalo envuelto en papel de regalo. El regalo (la función) sigue siendo el mismo, pero por fuera hay una capa que actúa primero. Un decorador es exactamente ese envoltorio: al llamar a la función, primero se ejecuta el código del envoltorio, que llama a la función original y puede hacer algo con el resultado.
Esto funciona porque en Python las funciones son objetos normales. Se pueden pasar a otras funciones, devolver desde funciones y declarar dentro de funciones. Los decoradores se basan en estas tres propiedades.
Cómo funciona un decorador: @ es azúcar sintáctico
Tomemos el decorador más sencillo, que imprime un mensaje antes y después de la llamada:
def registrar_llamadas(func):
def envoltorio():
print(f"Llamando a {func.__name__}")
resultado = func()
print(f"{func.__name__} ha terminado")
return resultado
return envoltorio
@registrar_llamadas
def saludar():
print("¡Hola!")
saludar()
Llamando a saludar
¡Hola!
saludar ha terminado
La línea @registrar_llamadas encima de def es totalmente equivalente a asignar después de la declaración:
def saludar():
print("¡Hola!")
saludar = registrar_llamadas(saludar)
Ahora el nombre saludar apunta a envoltorio, y la función original vive dentro de la clausura: envoltorio recuerda la variable func de la función externa. Un detalle importante: el decorador se aplica una sola vez, en el momento de ejecutar def, es decir, al importar el módulo, mientras que el envoltorio se ejecuta en cada llamada.
Clausuras y decoradores
Una clausura es una función interna que recuerda las variables de la función externa aunque esta ya haya terminado. Sin clausuras no existirían los decoradores: así es como envoltorio accede a func. Dentro de una clausura también se puede guardar un estado, por ejemplo un contador de llamadas:
def contar_llamadas(func):
llamadas = 0
def envoltorio(*args, **kwargs):
nonlocal llamadas
llamadas += 1
print(f"{func.__name__}: llamada n.º {llamadas}")
return func(*args, **kwargs)
return envoltorio
Cómo escribir un decorador en Python: plantilla universal
El primer ejemplo tiene dos inconvenientes: el envoltorio no acepta argumentos y «se come» el nombre de la función original. La plantilla de trabajo con la que conviene empezar cualquier decorador es esta:
import functools
def mi_decorador(func):
@functools.wraps(func)
def envoltorio(*args, **kwargs):
# código antes de la llamada
resultado = func(*args, **kwargs)
# código después de la llamada
return resultado
return envoltorio
Tres reglas de la plantilla:
*args, **kwargs— el envoltorio acepta cualquier argumento y lo pasa sin cambios.return resultado— no olvide devolver el resultado; si no, la función decorada siempre devolveráNone.@functools.wraps(func)— traslada los metadatos de la función original al envoltorio.
Ejemplo de decorador para medir el tiempo de ejecución:
import functools
import time
def cronometro(func):
@functools.wraps(func)
def envoltorio(*args, **kwargs):
inicio = time.perf_counter()
resultado = func(*args, **kwargs)
transcurrido = time.perf_counter() - inicio
print(f"{func.__name__} tardó {transcurrido:.3f} s")
return resultado
return envoltorio
@cronometro
def suma_lenta(n):
return sum(range(n))
suma_lenta(10_000_000) # suma_lenta tardó 0.142 s
Para qué sirve functools.wraps
Sin wraps, la función decorada pierde su nombre, su docstring y otros atributos: desde fuera se ve envoltorio:
def simple(func):
def envoltorio(*args, **kwargs):
return func(*args, **kwargs)
return envoltorio
@simple
def sumar(a, b):
"""Suma dos números."""
return a + b
print(sumar.__name__, sumar.__doc__) # envoltorio None
Con @functools.wraps(func), el envoltorio recibe __name__, __doc__, __module__, __qualname__ y __annotations__ de la función original, y en el atributo __wrapped__ queda una referencia al original. Esto es importante para depurar y para leer los trazados de error, para help(), para los frameworks que registran funciones por su nombre y para inspect.signature, que muestra la firma real.
Decorador con parámetros (con argumentos)
A veces el decorador necesita configuración: @reintentar(veces=3), @app.route("/"). Para eso se añade un nivel más de anidamiento. La función externa recibe los parámetros y devuelve el decorador propiamente dicho; a esta construcción se la llama fábrica de decoradores.
import functools
def repetir(veces):
def decorador(func):
@functools.wraps(func)
def envoltorio(*args, **kwargs):
resultado = None
for _ in range(veces):
resultado = func(*args, **kwargs)
return resultado
return envoltorio
return decorador
@repetir(veces=3)
def ping():
print("ping")
ping() # ping, ping, ping — tres líneas
El orden de las llamadas es el siguiente: repetir(veces=3) devuelve decorador, que se aplica a ping y devuelve envoltorio. Dicho de otro modo, @repetir(veces=3) equivale a ping = repetir(veces=3)(ping).
Un ejemplo práctico: repetir al producirse un error:
import functools
import time
def reintentar(veces=3, espera=1.0, excepciones=(Exception,)):
def decorador(func):
@functools.wraps(func)
def envoltorio(*args, **kwargs):
for intento in range(1, veces + 1):
try:
return func(*args, **kwargs)
except excepciones:
if intento == veces:
raise
time.sleep(espera)
return envoltorio
return decorador
@reintentar(veces=5, espera=0.5, excepciones=(ConnectionError,))
def obtener_tasa():
...
Varios decoradores en la misma función
Los decoradores se pueden apilar. Se aplican de abajo arriba: el más cercano a def es el primero.
@cronometro
@registrar_llamadas
def informe():
...
# lo mismo:
informe = cronometro(registrar_llamadas(informe))
Al llamar, la capa externa (cronometro) se ejecuta primero, así que medirá también el tiempo que tarda registrar_llamadas. El orden importa, por ejemplo, si un decorador comprueba los permisos de acceso y otro guarda el resultado en caché.
Decoradores estándar y integrados de Python
Muchos decoradores útiles ya vienen con el lenguaje y con la biblioteca estándar:
| Decorador | Qué hace |
|---|---|
@property | convierte un método en un atributo de solo lectura; con @x.setter, en un atributo controlado |
@staticmethod | método de clase sin self, una función normal dentro del espacio de nombres de la clase |
@classmethod | el método recibe la clase cls en lugar de la instancia; así se escriben los constructores alternativos |
@functools.cache, @functools.lru_cache | guardan en caché los resultados según los argumentos |
@functools.wraps | copia los metadatos de la función al envoltorio |
@dataclasses.dataclass | genera __init__, __repr__ y __eq__ para la clase |
@contextlib.contextmanager | convierte un generador en un gestor de contexto para with |
Caché: functools.cache y lru_cache
Un decorador de caché guarda el resultado para cada combinación de argumentos y, al repetir la llamada, devuelve el valor guardado sin volver a calcularlo. El ejemplo clásico son los números de Fibonacci recursivos, que sin caché tardan una eternidad:
import functools
@functools.cache
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(100)) # 354224848179261915075
functools.cache (disponible desde Python 3.9) guarda todos los resultados sin límite. @functools.lru_cache(maxsize=128) conserva solo las entradas más recientes y descarta las antiguas, lo que es más seguro en servicios de larga duración. Las estadísticas se consultan con fib.cache_info() y la caché se vacía con fib.cache_clear(). Los argumentos de estas funciones deben ser hashables: una lista como argumento provoca TypeError: unhashable type: 'list'. Además, conviene usar caché solo con funciones puras, es decir, aquellas que con los mismos argumentos siempre devuelven lo mismo y no tienen efectos secundarios.
Qué hace el decorador property
@property permite acceder a un método como si fuera un atributo, sin paréntesis, y controlar la asignación si hace falta:
class Cuenta:
def __init__(self, saldo):
self._saldo = saldo
@property
def saldo(self):
return self._saldo
@saldo.setter
def saldo(self, valor):
if valor < 0:
raise ValueError("El saldo no puede ser negativo")
self._saldo = valor
cuenta = Cuenta(100)
print(cuenta.saldo) # 100
cuenta.saldo = -5 # ValueError
staticmethod y classmethod
class Dinero:
def __init__(self, céntimos):
self.céntimos = céntimos
@classmethod
def desde_euros(cls, euros):
return cls(round(euros * 100))
@staticmethod
def es_valido(valor):
return valor >= 0
d = Dinero.desde_euros(12.5)
print(d.céntimos, Dinero.es_valido(-1)) # 1250 False
Es @classmethod el que permite a un método trabajar con la propia clase y no con sus instancias: desde_euros crea correctamente también un objeto de una subclase.
Decoradores de clases y clases como decoradores
Un decorador puede aplicarse a una clase: recibe la clase y devuelve una clase (la misma o modificada). Así funciona @dataclass. Un ejemplo sencillo es un registro de clases:
registro = {}
def registrar(cls):
registro[cls.__name__] = cls
return cls
@registrar
class ExportadorCsv:
pass
print(registro) # {'ExportadorCsv': <class '__main__.ExportadorCsv'>}
La situación inversa es una clase usada como decorador. Sirve cualquier objeto con un método __call__, y el estado se guarda cómodamente en los atributos de la instancia:
import functools
class ContarLlamadas:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.llamadas = 0
def __call__(self, *args, **kwargs):
self.llamadas += 1
return self.func(*args, **kwargs)
@ContarLlamadas
def tic():
pass
tic(); tic()
print(tic.llamadas) # 2
Decorador asíncrono
En las funciones async def, el envoltorio también debe ser una corrutina y esperar el resultado con await; de lo contrario, se devolvería un objeto de corrutina sin ejecutar:
import functools
def registrar_async(func):
@functools.wraps(func)
async def envoltorio(*args, **kwargs):
print(f"Inicio de {func.__name__}")
resultado = await func(*args, **kwargs)
print(f"Listo {func.__name__}")
return resultado
return envoltorio
Para qué sirven los decoradores en Python
- Lógica transversal — registro de eventos, métricas y medición del tiempo sin copiar código en cada función.
- Caché —
@functools.cachepara cálculos costosos y puros. - Comprobaciones — permisos, validación de argumentos y autorización en los manejadores web.
- Registro — rutas en Flask y FastAPI (
@app.get("/")) y fixtures de pytest. - Fiabilidad — reintentos, tiempos de espera y gestión de errores.
La gran ventaja es la separación de responsabilidades: la función se ocupa de su tarea, y el comportamiento auxiliar se añade con una sola línea y se quita con la misma facilidad.
Errores típicos
- El envoltorio no devuelve el resultado: la función decorada devuelve
None. - Faltan
*args, **kwargs:TypeErroral llamar con argumentos. - Olvidar
wraps:__name__y el docstring incorrectos y confusión en los registros. - Olvidar los paréntesis de la fábrica:
@repetiren lugar de@repetir(3)hace que la función caiga en el parámetroveces. - Código pesado en el cuerpo del decorador: se ejecuta al importar el módulo, no al llamar a la función.
Resumen
Los decoradores en Python son funciones de orden superior: reciben una función, devuelven un envoltorio, y la sintaxis @ es solo una forma breve de escribir una asignación. Empiece con la plantilla de *args, **kwargs, return resultado y functools.wraps, y para las configuraciones añada un nivel de fábrica. Las funciones, las clausuras y las anotaciones en las que se apoyan los decoradores se practican en el curso de Python.
Preguntas frecuentes
¿Se puede quitar un decorador de una función?
Si el decorador usa functools.wraps, el original está disponible en el atributo __wrapped__: func.__wrapped__(...) llama a la función sin el envoltorio. En caso contrario, solo queda quitar en el código la línea con @.
¿Cuándo se ejecuta el código del decorador?
El cuerpo del propio decorador se ejecuta una vez, al declarar la función, normalmente durante la importación del módulo. El código dentro de wrapper se ejecuta en cada llamada a la función decorada.
¿Se pueden decorar métodos de una clase?
Sí, un decorador normal con *args, **kwargs funciona también con métodos: self simplemente será el primer elemento de args.
¿En qué se diferencia un decorador de la herencia?
La herencia cambia el comportamiento de una clase mediante una jerarquía; un decorador envuelve desde fuera una función o una clase concreta y se combina con otros decoradores sin modificar el código.
Este tema se explica a fondo y con práctica en el curso «Python 3.15: от новичка до профи».
Ir al curso