Los decoradores son una de las funcionalidades más potentes y elegantes de Python. Permiten modificar el comportamiento de funciones y métodos de forma flexible y reutilizable, sin modificar su código fuente original. ¡Si alguna vez usaste @staticmethod, @classmethod o @property, ya has trabajado con decoradores!

En esta guía completa, aprenderás desde lo básico hasta técnicas avanzadas de decoradores, con ejemplos prácticos que puedes aplicar inmediatamente en tus proyectos.

¿Qué Son los Decoradores?

En Python, un decorador es una función que recibe otra función como argumento y extiende su comportamiento sin modificar explícitamente su código. Es como envolver un "regalo" alrededor de una función para agregar funcionalidad extra.

La sintaxis de decoradores usa el símbolo @ seguido del nombre del decorador, colocado justo encima de la definición de una función:

@mi_decorador
def mi_funcion():
    pass

Esta sintaxis es equivalente a:

def mi_funcion():
    pass

mi_funcion = mi_decorador(mi_funcion)

Entender esta equivalencia es fundamental para comprender cómo funcionan los decoradores "entre bastidores".

Decoradores Integrados de Python

Python ya viene con varios decoradores nativos que puedes usar inmediatamente en tus proyectos.

@staticmethod

El decorador @staticmethod define un método estático dentro de una clase. Los métodos estáticos no reciben el parámetro self (instancia) ni cls (clase), siendo útiles para funcionalidades que no necesitan acceder a atributos de la clase.

class Calculadora:
    @staticmethod
    def sumar(a, b):
        return a + b
@staticmethod
def multiplicar(a, b):
    return a * b

Llamando sin crear una instancia

print(Calculadora.sumar(5, 3)) # Salida: 8 print(Calculadora.multiplicar(4, 2)) # Salida: 8

Según la documentación oficial de Python, los métodos estáticos son similares a los métodos en [C++](https://en.wikipedia.org/wiki/C%2B%2B) o [Java](https://www.wikipedia.org/wiki/Java_(programming_language)). La documentación oficial está disponible en [python.org](https://docs.python.org/3/library/functions.html#staticmethod).

@classmethod

El decorador @classmethod define un método de clase que recibe la clase como primer argumento (convencionalmente llamado cls). Esto permite acceder y modificar atributos de clase, crear factory methods y manipular la clase misma.

class Persona:
    total_personas = 0
def __init__(self, nombre, edad):
    self.nombre = nombre
    self.edad = edad
    Persona.total_personas += 1

@classmethod
def crear_cumpleanos(cls, nombre):
    """Factory method para crear persona con 0 años"""
    return cls(nombre, 0)

@classmethod
def obtener_total(cls):
    return cls.total_personas

@classmethod
def cambiar_total(cls, nuevo_valor):
    cls.total_personas = nuevo_valor

p1 = Persona("Ana", 25) p2 = Persona.crear_cumpleanos("João") print(Persona.obtener_total()) # Salida: 2

Los métodos de clase son especialmente útiles para constructores alternativos, como muestran los ejemplos en [PEP 8](https://peps.python.org/pep-0008/) y la [documentación oficial](https://docs.python.org/3/library/functions.html#classmethod).

@property

El decorador @property permite definir métodos que se comportan como atributos, permitiendo agregar lógica de getter, setter y deleter de forma controlada.

class Temperatura:
    def __init__(self, celsius):
        self._celsius = celsius
@property
def celsius(self):
    return self._celsius

@property
def fahrenheit(self):
    return (self._celsius * 9/5) + 32

@property
def kelvin(self):
    return self._celsius + 273.15

@celsius.setter
def celsius(self, valor):
    if valor < -273.15:
        raise ValueError("La temperatura no puede ser menor que el cero absoluto")
    self._celsius = valor

temp = Temperatura(25) print(temp.fahrenheit) # Salida: 77.0 print(temp.kelvin) # Salida: 298.15 temp.celsius = 30 print(temp.celsius) # Salida: 30

El decorador @property es fundamental para el encapsulamiento en Python. Más información puede encontrarse en la [documentación oficial de Python](https://docs.python.org/3/library/functions.html#property) y el [Python Cookbook](https://www.oreilly.com/library/view/python-cookbook-3rd/9781440597339/).

Creando Tu Propio Decorador

Ahora que conoces los decoradores integrados, ¡aprendamos a crear los tuyos!

Decorador Simple

Un decorador personalizado es simplemente una función que recibe una función y devuelve una nueva:

def mi_decorador(func):
    def wrapper():
        print("Antes de la función")
        func()
        print("Después de la función")
    return wrapper

@mi_decorador def decir_hola(): print("¡Hola!")

decir_hola()

Salida:

Antes de la función
¡Hola!
Después de la función

Decorador con Argumentos

Para crear decoradores que aceptan argumentos, necesitamos una capa más de funciones:

def repetir(veces):
    def decorador(func):
        def wrapper(*args, **kwargs):
            for _ in range(veces):
                resultado = func(*args, **kwargs)
            return resultado
        return wrapper
    return decorador

@repetir(3) def saludar(nombre): print(f"¡Hola, {nombre}!")

saludar("María")

Salida:

¡Hola, María!
¡Hola, María!
¡Hola, María!

Preservando Metadatos de la Función

Un problema común al crear decoradores es perder los metadatos de la función original (como __name__, __doc__, etc.). Para preservarlos, usamos functools.wraps:

import functools

def decorador_log(func): @functools.wraps(func) def wrapper(*args, *kwargs): print(f"Llamando a {func.name}") resultado = func(args, **kwargs) print(f"{func.name} finalizada") return resultado return wrapper

@decorador_log def sumar(a, b): """Suma dos números""" return a + b

print(sumar.name) # Salida: sumar (¡no wrapper!) print(sumar.doc) # Salida: Suma dos números

Sin @functools.wraps, el nombre sería "wrapper" y la docstring se perdería. Esta técnica se explica en la [documentación de functools](https://docs.python.org/3/library/functools.html).

Decoradores con Argumentos Personalizados

Vamos a crear un decorador más útil: uno que mide el tiempo de ejecución de funciones:

import functools
import time

def temporizador(func): @functools.wraps(func) def wrapper(*args, *kwargs): inicio = time.time() resultado = func(args, **kwargs) fin = time.time() print(f"{func.name} se ejecutó en {fin - inicio:.4f} segundos") return resultado return wrapper

@temporizador def procesar_datos(lista): total = 0 for item in lista: total += item ** 2 return total

resultado = procesar_datos(range(1000)) print(f"Resultado: {resultado}")

Decorador de Reintento

Otro ejemplo útil: decorador que intenta ejecutar la función de nuevo si falla:

import functools
import time

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

@reintentar(max_intentos=3, delay=1) def conectar_api(): import random if random.random() > 0.7: return "¡Conexión establecida!" raise ConnectionError("API no disponible")

print(conectar_api())

Decoradores para Clases

¡También puedes aplicar decoradores a clases! Un ejemplo famoso es el decorador @dataclass del módulo dataclasses:

from dataclasses import dataclass
from typing import List

@dataclass class Producto: nombre: str precio: float cantidad: int = 0

def total(self):
    return self.precio * self.cantidad

producto = Producto("Notebook", 2500.00, 3) print(producto) print(f"Total: R$ {producto.total()}")

Salida:

Producto(nombre='Notebook', precio=2500.0, cantidad=3)
Total: R$ 7500.0

También existen decoradores como @singledispatch (para funciones sobrecargadas), @lru_cache (para memorización), y más. La documentación completa está disponible en [docs.python.org](https://docs.python.org/3/library/functools.html).

Decoradores con Estado

Podemos crear decoradores que mantienen estado entre llamadas usando closures:

import functools

def cache(func): @functools.wraps(func) def wrapper(args): if args not in wrapper.cache: wrapper.cache[args] = func(args) return wrapper.cache[args] wrapper.cache = {} return wrapper

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

print(fibonacci(100)) # Calculado una vez print(fibonacci(100)) # Recuperado del caché print(fibonacci(50)) # Calculado print(fibonacci(50)) # Recuperado del caché

Pila de Decoradores

Puedes aplicar múltiples decoradores a la misma función. Se aplican de abajo hacia arriba:

import functools

def decorador_a(func): @functools.wraps(func) def wrapper(*args, *kwargs): print("A - Antes") resultado = func(args, **kwargs) print("A - Después") return resultado return wrapper

def decorador_b(func): @functools.wraps(func) def wrapper(*args, *kwargs): print("B - Antes") resultado = func(args, **kwargs) print("B - Después") return resultado return wrapper

@decorador_a @decorador_b def mi_funcion(): print("Mi función ejecutándose")

mi_funcion()

Salida:

A - Antes
B - Antes
Mi función ejecutándose
B - Después
A - Después

Clases como Decoradores

Además de funciones, puedes usar clases como decoradores implementando el método __call__:

import functools

class DecoradorDeClase: def init(self, func): self.func = func functools.update_wrapper(self, func)

def __call__(self, *args, **kwargs):
    print("Antes de la llamada")
    resultado = self.func(*args, **kwargs)
    print("Después de la llamada")
    return resultado

@DecoradorDeClase def funcion_prueba(): print("Función ejecutándose")

funcion_prueba()

Uso Real: Flask y Django

Los decoradores son ampliamente usados en frameworks web. En Flask, por ejemplo:

from functools import wraps

def login_required(f): @wraps(f) def decorated_function(*args, *kwargs): if not current_user.is_authenticated: return redirect(url_for('login')) return f(args, **kwargs) return decorated_function

@app.route('/perfil') @login_required def perfil(): return render_template('perfil.html')

En Django, los decoradores se usan para control de permisos y autenticación. Más información sobre decoradores en frameworks puede encontrarse en la [documentación de Flask](https://flask.palletsprojects.com/) y [Django](https://docs.djangoproject.com/).

Mejores Prácticas

Ahora que dominas los decoradores, aquí tienes algunas mejores prácticas:

1. Siempre usa @functools.wraps

import functools

def mi_decorador(func): @functools.wraps(func) # Preserva metadatos def wrapper(*args, *kwargs): return func(args, **kwargs) return wrapper

2. Documenta tus decoradores

def obsoleto(razon):
    """Decorador para marcar funciones como obsoletas.
Args:
    razon: String explicando por qué la función está obsoleta.
"""
def decorador(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        import warnings
        warnings.warn(
            f"{func.__name__} está obsoleta: {razon}",
            DeprecationWarning,
            stacklevel=2
        )
        return func(*args, **kwargs)
    return wrapper
return decorador

@obsoleto("Usa nueva_funcion en su lugar") def funcion_antigua(): pass

3. Usa *args y **kwargs

def validar_args(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # Validar argumentos antes
        return func(*args, **kwargs)
    return wrapper

4. Cuidado con decoradores en métodos de instancia

class MiClase:
    @mi_decorador
    def metodo(self):
        # El decorador debe usar *args, **kwargs
        pass

Decoradores Avanzados

Decoradores con opciones de configuración

import functools

def configurar(*config): def decorador(func): @functools.wraps(func) def wrapper(args, **kwargs): if config.get('log', False): print(f"Ejecutando {func.name}") if config.get('cache', False):

Implementar caché

            pass
        return func(*args, **kwargs)
    return wrapper
return decorador

@configurar(log=True, cache=True) def procesar(): pass

Decorador de validación de tipos

import functools

def verificar_tipos(tipos): def decorador(func): @functools.wraps(func) def wrapper(*args, *kwargs): for nombre, tipo in tipos.items(): if nombre in kwargs: if not isinstance(kwargs[nombre], tipo): raise TypeError( f"{nombre} debe ser {tipo.name}, " f"recibido {type(kwargs[nombre]).name}" ) return func(args, kwargs) return wrapper return decorador

@verificar_tipos(edad=int, nombre=str) def crear_usuario(nombre, edad): return f"Usuario {nombre}, {edad} años"

print(crear_usuario(nombre="João", edad=25)) # OK

print(crear_usuario(nome="João", edad="25")) # TypeError

Cuándo Usar Decoradores

Los decoradores son ideales para:

  • Logging: Registrar llamadas de funciones
  • Autenticación y autorización: Verificar permisos
  • Caché: Almacenar resultados de funciones costosas
  • Validación: Verificar argumentos antes de la ejecución
  • Instrumentación: Medir tiempo de ejecución
  • Reintento: Intentar de nuevo en caso de fallo

Decoradores vs Wrappers

Es importante entender la diferencia:

Decoradores son la sintaxis @decorador aplicada a funciones o clases. Son el concepto general.

Wrappers (o wrappers) son las funciones internas que reciben la función original y agregan comportamiento.

En el ejemplo:

def decorador(func):  # "decorador" es la función decoradora
    def wrapper(*args, **kwargs):  # "wrapper" es el wrapper
        return func(*args, **kwargs)
    return wrapper

Próximos Pasos

Ahora que dominas los decoradores, sigue aprendiendo:

Los decoradores son fundamentales para escribir código Pythonico y reutilizable. ¡Practica creando tus propios decoradores y verás una mejora significativa en la calidad de tu código!

Para más contenido sobre Python y desarrollo, ¡sigue siguiendo Universo Python!