▷ Decoradores en Python explicados — De cero a usarlos como un pro 2026

Decoradores en Python explicados — De cero a usarlos como un pro

Si llevas un tiempo programando Python, tarde o temprano te encuentras con una línea tipo @algo justo encima de una función. Y si nunca te lo ha explicado nadie en condiciones, la primera reacción es: “¿esto qué hace exactamente?”.

Los decoradores tienen fama de tema avanzado, pero en realidad son una idea muy simple disfrazada de sintaxis rara. Una vez los entiendes a fondo, los ves por todos lados: en Flask (@app.route), en pytest (@pytest.fixture), en clases (@property, @staticmethod), en logging, caching, autenticación…

Esta entrada te lleva de cero a entenderlos a fondo. No “saber usarlos” — entender qué pasa por debajo, que es lo único que te permite escribir los tuyos sin copiar de Stack Overflow.

La idea en 30 segundos

Un decorador es una función que toma otra función y devuelve una función (normalmente una versión modificada de la original). El símbolo @ es solo azúcar sintáctico para llamarlo.

Esto…

@mi_decorador
def saludar():
    print("Hola")

…es exactamente equivalente a esto:

def saludar():
    print("Hola")

saludar = mi_decorador(saludar)

Y ya está. Eso es todo. El resto de la entrada es desarrollar este concepto hasta dominarlo.

Pre-requisito: las funciones son objetos

Para entender decoradores tienes que tener clarísimo que en Python las funciones son objetos de primera clase. Significa tres cosas:

def saludar(nombre):
    return f"Hola, {nombre}"

# 1. Las funciones se asignan a variables
mi_funcion = saludar
print(mi_funcion("Ana"))   # "Hola, Ana"

# 2. Las funciones se pasan como argumento a otras funciones
def aplicar(funcion, valor):
    return funcion(valor)

print(aplicar(saludar, "Pedro"))  # "Hola, Pedro"

# 3. Las funciones se devuelven desde otras funciones
def crear_saludador():
    def hablar(nombre):
        return f"Buenas, {nombre}"
    return hablar

mi_saludador = crear_saludador()
print(mi_saludador("Luis"))  # "Buenas, Luis"

Si esto te suena raro, tómate 5 minutos. Sin esto, los decoradores no encajan.

Tu primer decorador (sin azúcar)

Vamos a escribir uno sin usar la sintaxis @ para que veas que no hay magia.

Caso: queremos medir cuánto tarda en ejecutarse una función.

import time

def medir_tiempo(funcion):
    def wrapper(*args, **kwargs):
        inicio = time.time()
        resultado = funcion(*args, **kwargs)
        fin = time.time()
        print(f"{funcion.__name__} tardó {fin - inicio:.4f}s")
        return resultado
    return wrapper


def trabajar_duro():
    time.sleep(1)
    return "Hecho"


# Sin @: aplicamos el decorador a mano
trabajar_duro = medir_tiempo(trabajar_duro)

print(trabajar_duro())
# trabajar_duro tardó 1.0010s
# Hecho

Lo que pasa, paso a paso:

  1. medir_tiempo es una función que recibe otra función (funcion) y devuelve una nueva (wrapper).
  2. wrapper ejecuta la función original metida entre dos time.time(), imprime cuánto tardó y devuelve su resultado.
  3. Al hacer trabajar_duro = medir_tiempo(trabajar_duro), reemplazamos la función original por la nueva versión envuelta.
  4. Cuando llamamos a trabajar_duro(), en realidad estamos llamando al wrapper — pero a efectos prácticos parece que es la misma función, solo que ahora mide su tiempo.

📥 Llévate el cheatsheet de Python (gratis)

PDF de 6 páginas con lo esencial: tipos, condicionales, bucles, estructuras de datos, funciones y los errores que más vas a cometer. Para tener al lado mientras programas.

Sin spam. Te apuntas a la lista, descargas el cheatsheet y recibes contenido de Python cada semana.

Y ahora con la sintaxis @

Lo mismo, escrito con la sintaxis bonita:

import time

def medir_tiempo(funcion):
    def wrapper(*args, **kwargs):
        inicio = time.time()
        resultado = funcion(*args, **kwargs)
        fin = time.time()
        print(f"{funcion.__name__} tardó {fin - inicio:.4f}s")
        return resultado
    return wrapper


@medir_tiempo
def trabajar_duro():
    time.sleep(1)
    return "Hecho"


print(trabajar_duro())

@medir_tiempo encima de trabajar_duro es literalmente trabajar_duro = medir_tiempo(trabajar_duro). Mismo código, otra cara.

Por qué *args y **kwargs dentro del wrapper

Porque tu decorador no sabe qué argumentos va a recibir la función decorada. Si escribes def wrapper(): (sin parámetros), tu decorador solo funcionará con funciones sin argumentos.

@medir_tiempo
def sumar(a, b):
    return a + b

print(sumar(2, 3))  # ¿funciona?

Sí, funciona — porque wrapper(*args, **kwargs) capta cualquier combinación: posicionales, nombrados, los que sean. Y los reenvía con funcion(*args, **kwargs). Es el patrón estándar para decoradores genéricos.

El problema del __name__ (y functools.wraps)

Hay un efecto secundario molesto al decorar:

@medir_tiempo
def trabajar_duro():
    """Hace cosas pesadas."""
    time.sleep(1)

print(trabajar_duro.__name__)   # "wrapper"   ← ¡no es "trabajar_duro"!
print(trabajar_duro.__doc__)    # None        ← se perdió el docstring

El motivo: la función ahora apunta al wrapper, no a la original. Para herramientas como help(), debugging, o frameworks que inspeccionan funciones, esto es un dolor.

Solución: functools.wraps. Es un decorador (sí, decorador de decorador) que copia el nombre, docstring y metadata de la función original al wrapper:

from functools import wraps
import time

def medir_tiempo(funcion):
    @wraps(funcion)                        # ← esta línea
    def wrapper(*args, **kwargs):
        inicio = time.time()
        resultado = funcion(*args, **kwargs)
        print(f"{funcion.__name__} tardó {time.time() - inicio:.4f}s")
        return resultado
    return wrapper


@medir_tiempo
def trabajar_duro():
    """Hace cosas pesadas."""
    time.sleep(1)

print(trabajar_duro.__name__)   # "trabajar_duro"   ✓
print(trabajar_duro.__doc__)    # "Hace cosas pesadas."   ✓

💡 Regla: siempre usa @wraps(funcion) en tus decoradores. No tiene downside y te ahorra dolores de cabeza futuros.

Decoradores con argumentos

Hasta ahora @medir_tiempo sin paréntesis. Pero verás cosas como @app.route("/users") o @retry(intentos=3). Eso es un decorador con argumentos, y necesita una capa más.

La idea: si quieres pasar parámetros a tu decorador, tienes que escribir una función que devuelve un decorador. Es decir: una función que devuelve una función que devuelve una función. Sí, suena absurdo.

Ejemplo: decorador que reintenta la función N veces si falla:

from functools import wraps
import time

def reintentar(intentos=3, espera=1):
    def decorador(funcion):
        @wraps(funcion)
        def wrapper(*args, **kwargs):
            for intento in range(1, intentos + 1):
                try:
                    return funcion(*args, **kwargs)
                except Exception as e:
                    print(f"Intento {intento} falló: {e}")
                    if intento == intentos:
                        raise
                    time.sleep(espera)
        return wrapper
    return decorador


@reintentar(intentos=3, espera=2)
def conectar_api():
    # imagina aquí una llamada que falla a veces
    ...

Tres niveles:

  1. reintentar(intentos=3, espera=2) → recibe los argumentos del decorador y devuelve…
  2. decorador(funcion) → un decorador que recibe la función a decorar y devuelve…
  3. wrapper(*args, **kwargs) → la función envuelta que se llama de verdad.

Cuando escribes @reintentar(intentos=3), Python ejecuta reintentar(intentos=3) (que devuelve el decorador) y luego aplica ese decorador a la función. Sintaxis confusa, idea simple.

Decoradores apilados

Puedes poner varios @ encima de una función. Se aplican de abajo hacia arriba:

@medir_tiempo
@reintentar(intentos=3)
def conectar():
    ...

Equivale a conectar = medir_tiempo(reintentar(intentos=3)(conectar)).

Es decir: primero envuelves con reintentar, después envuelves el resultado con medir_tiempo. Orden importa: en este ejemplo, medir_tiempo mide el tiempo total incluyendo los reintentos. Si pusieras @reintentar arriba, mediría cada intento por separado.

Casos reales donde verás (y querrás) decoradores

1. Logging automático

from functools import wraps
import logging

logger = logging.getLogger(__name__)

def log_llamada(funcion):
    @wraps(funcion)
    def wrapper(*args, **kwargs):
        logger.info(f"Llamando a {funcion.__name__} con args={args}, kwargs={kwargs}")
        return funcion(*args, **kwargs)
    return wrapper

@log_llamada
def crear_usuario(nombre, email):
    ...

2. Caché simple (memoization)

Para no recomputar lo mismo dos veces:

from functools import wraps

def cachear(funcion):
    cache = {}
    @wraps(funcion)
    def wrapper(*args):
        if args not in cache:
            cache[args] = funcion(*args)
        return cache[args]
    return wrapper

@cachear
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(50))   # Instantáneo. Sin cache, tarda minutos.

En la práctica usa functools.lru_cache — el decorador de la librería estándar hace esto y mejor. Pero entender el código de arriba te enseña qué hace por dentro.

3. Validación / autenticación (estilo Flask)

from functools import wraps

def requiere_admin(funcion):
    @wraps(funcion)
    def wrapper(usuario, *args, **kwargs):
        if not usuario.es_admin:
            raise PermissionError("Solo admins pueden hacer esto")
        return funcion(usuario, *args, **kwargs)
    return wrapper

@requiere_admin
def borrar_usuario(usuario, id_a_borrar):
    ...

4. Decoradores de librerías típicas

@app.route("/users/<id>")          # Flask: registra la URL
@pytest.fixture                    # Pytest: marca un fixture
@dataclass                         # Standard: convierte clase en dataclass
@property                          # Standard: convierte método en propiedad
@staticmethod                      # Standard: método estático
@cached_property                   # Standard: propiedad cacheada

Todos siguen exactamente las reglas que has visto.

Decoradores en clases (como decorador)

También puedes decorar métodos de clases. Funciona igual; solo recuerda que el primer argumento del wrapper será self:

from functools import wraps

def log_metodo(funcion):
    @wraps(funcion)
    def wrapper(self, *args, **kwargs):
        print(f"Llamando {self.__class__.__name__}.{funcion.__name__}")
        return funcion(self, *args, **kwargs)
    return wrapper


class Usuario:
    def __init__(self, nombre):
        self.nombre = nombre

    @log_metodo
    def saludar(self):
        return f"Hola, soy {self.nombre}"


u = Usuario("Ana")
u.saludar()
# Llamando Usuario.saludar

Decoradores escritos como clase

Avanzado, pero útil saber que existe. Una clase con __call__ también puede actuar como decorador:

class ContadorLlamadas:
    def __init__(self, funcion):
        self.funcion = funcion
        self.llamadas = 0

    def __call__(self, *args, **kwargs):
        self.llamadas += 1
        print(f"{self.funcion.__name__} llamada #{self.llamadas}")
        return self.funcion(*args, **kwargs)


@ContadorLlamadas
def saludar():
    print("Hola")


saludar()  # saludar llamada #1
saludar()  # saludar llamada #2
print(saludar.llamadas)  # 2

Útil cuando el decorador necesita mantener estado complejo. Para casos simples, una función con closure (la versión clásica) es más legible.

Errores típicos

# 1. Olvidar return en el wrapper
def malo(funcion):
    @wraps(funcion)
    def wrapper(*args, **kwargs):
        funcion(*args, **kwargs)   # ← FALTA return
    return wrapper

# Ahora todas las funciones decoradas devuelven None aunque retornaran algo.

# 2. Olvidar que reintentar(3) y reintentar son cosas distintas
@reintentar          # ← MAL: pasa la función a reintentar como si fuera "intentos"
def algo(): ...

@reintentar()        # ← BIEN: ejecuta reintentar() para obtener el decorador
def algo(): ...

# 3. Aplicar dos decoradores y confundirse con el orden
@a
@b
def f(): ...
# Equivale a a(b(f)), no b(a(f)). Lee de abajo hacia arriba.

Resumen

ConceptoRegla
Decorador básicoFunción que toma una función y devuelve otra
Sintaxis @Azúcar para f = decorador(f)
Pasar args genéricosdef wrapper(*args, **kwargs): ...
Conservar metadataSiempre @functools.wraps(funcion)
Decorador con argsTres niveles: def deco(arg): def real(f): def wrapper: ...
ApiladosSe aplican de abajo hacia arriba
Clase como decoradorImplementa __call__

¿Te ha valido esto?

Si te ha resultado útil, llévate el cheatsheet de Python en PDF — 6 páginas con tipos, condicionales, bucles, estructuras de datos, funciones y los errores típicos. Para tener al lado mientras programas. Gratis.

Sin spam. Email + cheatsheet + un correo por semana con tutoriales nuevos.

Tu siguiente paso

Si has llegado hasta aquí, los decoradores ya no son magia para ti. Ahora viene la parte importante: escribir los tuyos en código real. La próxima vez que veas que estás repitiendo lógica al inicio o al final de varias funciones (logging, validación, caché, medición de tiempo, manejo de errores) — ahí tienes un decorador esperando.

Si quieres aprender Python desde la base, con funciones, clases, módulos y todo lo necesario para construir proyectos reales en los que estos patrones tienen sentido, en El Pythonista lo enseño paso a paso.

Un abrazo,
Oscar

¿Quieres aprender Python en orden, no a saltos?

Esto que has leído es solo una pieza. En El Pythonista lo verás todo encadenado: 11 módulos, 37+ horas de vídeo, 734 actividades y un proyecto real (MovieTracker) que crece contigo desde la primera variable hasta el deploy a producción.

Ver el curso completo →

37+ horas · 734 actividades · Proyecto real · Acceso de por vida · 14 días de garantía

Compartir

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Publicar un comentario