Fundamentos

Decoradores en Python: qué son, cómo funcionan y cómo escribir uno propio

Actualizado: · Skillaria.Top

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:

  1. *args, **kwargs — el envoltorio acepta cualquier argumento y lo pasa sin cambios.
  2. return resultado — no olvide devolver el resultado; si no, la función decorada siempre devolverá None.
  3. @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:

DecoradorQué hace
@propertyconvierte un método en un atributo de solo lectura; con @x.setter, en un atributo controlado
@staticmethodmétodo de clase sin self, una función normal dentro del espacio de nombres de la clase
@classmethodel método recibe la clase cls en lugar de la instancia; así se escriben los constructores alternativos
@functools.cache, @functools.lru_cacheguardan en caché los resultados según los argumentos
@functools.wrapscopia los metadatos de la función al envoltorio
@dataclasses.dataclassgenera __init__, __repr__ y __eq__ para la clase
@contextlib.contextmanagerconvierte 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.cache para 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: TypeError al llamar con argumentos.
  • Olvidar wraps: __name__ y el docstring incorrectos y confusión en los registros.
  • Olvidar los paréntesis de la fábrica: @repetir en lugar de @repetir(3) hace que la función caiga en el parámetro veces.
  • 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.

Aprende este tema en vídeo

Este tema se explica a fondo y con práctica en el curso «Python 3.15: от новичка до профи».

Ir al curso