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:
- Generadores Python — aprende sobre generadores e iteradores
- Context Managers — gestiona recursos con el protocolo context manager
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!