Decoradores: creación y aplicación
Introducción
Los decoradores son una característica poderosa de Python que permite modificar el comportamiento de funciones o clases sin cambiar su código. Esta funcionalidad avanzada forma parte del paradigma de programación funcional y representa una forma elegante de aplicar el principio de composición de funciones. Los decoradores nos permiten "envolver" una función con otra función para extender su comportamiento, agregar funcionalidades o modificar sus entradas y salidas, todo ello manteniendo el código limpio y siguiendo el principio DRY (Don't Repeat Yourself).
En este artículo, exploraremos qué son los decoradores, cómo crearlos y las formas más comunes de aplicarlos en nuestros programas Python.
Entendiendo los decoradores
En esencia, un decorador es una función que toma otra función como argumento, agrega alguna funcionalidad y devuelve otra función, todo sin modificar el código de la función original.
Para entender cómo funcionan los decoradores, recordemos que en Python las funciones son objetos de primera clase, lo que significa que pueden ser:
- Asignadas a variables
- Pasadas como argumentos a otras funciones
- Devueltas como resultado de otras funciones
Veamos un ejemplo sencillo:
def saludo():
return "¡Hola mundo!"
# Asignar una función a una variable
mi_funcion = saludo
print(mi_funcion()) # Imprime: ¡Hola mundo!
Funciones que devuelven funciones
Los decoradores se basan en la capacidad de Python para crear funciones que devuelven otras funciones:
def crear_funcion_saludo(nombre):
def saludar():
return f"¡Hola, {nombre}!"
return saludar
saludo_maria = crear_funcion_saludo("María")
print(saludo_maria()) # Imprime: ¡Hola, María!
Creación de un decorador básico
Ahora veamos cómo crear un decorador básico:
def mi_decorador(funcion):
def funcion_envoltorio():
print("Código que se ejecuta antes de la función")
funcion() # Llamamos a la función original
print("Código que se ejecuta después de la función")
return funcion_envoltorio
# Aplicación manual del decorador
def saludar():
print("¡Hola!")
saludar_decorada = mi_decorador(saludar)
saludar_decorada() # Ejecuta la función decorada
La salida sería:
Código que se ejecuta antes de la función
¡Hola!
Código que se ejecuta después de la función
Sintaxis de decoración con @
Python proporciona una sintaxis especial para aplicar decoradores utilizando el símbolo @
:
def mi_decorador(funcion):
def funcion_envoltorio():
print("Código que se ejecuta antes de la función")
funcion()
print("Código que se ejecuta después de la función")
return funcion_envoltorio
# Aplicación del decorador usando la sintaxis @
@mi_decorador
def saludar():
print("¡Hola!")
saludar() # Ahora saludar ya está decorada
Esta sintaxis @mi_decorador
es equivalente a saludar = mi_decorador(saludar)
.
Decoradores con argumentos de función
Los decoradores anteriores funcionan bien para funciones sin argumentos, pero ¿qué pasa con las funciones que tienen parámetros? Podemos adaptar nuestros decoradores así:
def mi_decorador(funcion):
def funcion_envoltorio(*args, **kwargs):
print("Código que se ejecuta antes de la función")
resultado = funcion(*args, **kwargs) # Llamamos a la función original con sus argumentos
print("Código que se ejecuta después de la función")
return resultado
return funcion_envoltorio
@mi_decorador
def suma(a, b):
return a + b
resultado = suma(5, 3) # Imprime los mensajes del decorador y devuelve 8
print(f"El resultado es: {resultado}")
Decoradores con parámetros
También podemos crear decoradores que acepten sus propios parámetros:
def repetir(n):
def decorador_repetir(funcion):
def funcion_envoltorio(*args, **kwargs):
resultado = None
for _ in range(n):
resultado = funcion(*args, **kwargs)
return resultado
return funcion_envoltorio
return decorador_repetir
@repetir(3)
def saludar(nombre):
print(f"¡Hola {nombre}!")
return f"Saludo a {nombre} completado"
resultado = saludar("Ana") # Imprime "¡Hola Ana!" tres veces
print(resultado) # Imprime: Saludo a Ana completado
En este caso, la estructura es un poco más compleja. Tenemos tres niveles de funciones:
repetir(n)
: La función exterior que acepta el argumento del decoradordecorador_repetir(funcion)
: El decorador real que recibe la funciónfuncion_envoltorio(*args, **kwargs)
: La función envoltorio que se ejecutará
Casos de uso comunes para decoradores
Los decoradores tienen muchas aplicaciones prácticas:
- Registrar acciones (logging):
def registrar(funcion):
def funcion_envoltorio(*args, **kwargs):
print(f"Llamando a la función: {funcion.__name__}")
resultado = funcion(*args, **kwargs)
print(f"La función {funcion.__name__} retornó: {resultado}")
return resultado
return funcion_envoltorio
@registrar
def multiplicar(a, b):
return a * b
multiplicar(4, 5)
- Medir tiempo de ejecución:
import time
def medir_tiempo(funcion):
def funcion_envoltorio(*args, **kwargs):
inicio = time.time()
resultado = funcion(*args, **kwargs)
fin = time.time()
print(f"La función {funcion.__name__} tardó {fin - inicio:.5f} segundos en ejecutarse")
return resultado
return funcion_envoltorio
@medir_tiempo
def proceso_lento():
time.sleep(1) # Simula un proceso que tarda 1 segundo
return "Proceso completado"
proceso_lento()
- Control de acceso:
def requiere_autenticacion(funcion):
def funcion_envoltorio(*args, **kwargs):
# Simulamos una comprobación de autenticación
usuario_autenticado = True # En un caso real, esto vendría de una sesión
if usuario_autenticado:
return funcion(*args, **kwargs)
else:
return "Acceso denegado. Por favor, inicie sesión."
return funcion_envoltorio
@requiere_autenticacion
def pagina_admin():
return "Bienvenido al panel de administración"
print(pagina_admin())
- Validación de datos:
def validar_enteros_positivos(funcion):
def funcion_envoltorio(*args, **kwargs):
# Validamos que todos los argumentos posicionales sean enteros positivos
if not all(isinstance(arg, int) and arg > 0 for arg in args):
raise ValueError("Todos los argumentos deben ser enteros positivos")
return funcion(*args, **kwargs)
return funcion_envoltorio
@validar_enteros_positivos
def calcular_area_rectangulo(ancho, alto):
return ancho * alto
try:
print(calcular_area_rectangulo(5, 10)) # Funciona correctamente
print(calcular_area_rectangulo(5, -2)) # Lanza un error
except ValueError as e:
print(e)
Encadenando decoradores
Podemos aplicar múltiples decoradores a una función, y se aplicarán de abajo hacia arriba:
def decorador_a(funcion):
def funcion_envoltorio(*args, **kwargs):
print("Decorador A - Antes")
resultado = funcion(*args, **kwargs)
print("Decorador A - Después")
return resultado
return funcion_envoltorio
def decorador_b(funcion):
def funcion_envoltorio(*args, **kwargs):
print("Decorador B - Antes")
resultado = funcion(*args, **kwargs)
print("Decorador B - Después")
return resultado
return funcion_envoltorio
@decorador_a
@decorador_b
def saludar():
print("¡Hola mundo!")
saludar()
La salida será:
Decorador A - Antes
Decorador B - Antes
¡Hola mundo!
Decorador B - Después
Decorador A - Después
Preservando metadatos de la función original
Un problema con los decoradores es que pierden los metadatos de la función original como el nombre, la documentación, etc. Para solucionar esto, podemos usar la función wraps
del módulo functools
:
from functools import wraps
def mi_decorador(funcion):
@wraps(funcion) # Preserva los metadatos de la función original
def funcion_envoltorio(*args, **kwargs):
"""Esta es la documentación del envoltorio"""
print("Antes de llamar a la función")
resultado = funcion(*args, **kwargs)
print("Después de llamar a la función")
return resultado
return funcion_envoltorio
@mi_decorador
def saludar(nombre):
"""Esta función saluda a la persona dada"""
return f"¡Hola, {nombre}!"
# Sin @wraps, esto mostraría el nombre y la doc de funcion_envoltorio
print(saludar.__name__) # Imprime: saludar
print(saludar.__doc__) # Imprime: Esta función saluda a la persona dada
Resumen
Los decoradores son una poderosa herramienta de Python que nos permite extender el comportamiento de funciones o clases sin modificar su código. Hemos aprendido a crear decoradores simples, decoradores con argumentos de función, decoradores que aceptan parámetros propios y hemos visto casos de uso comunes como registro, medición de tiempo, control de acceso y validación de datos.
El uso adecuado de decoradores puede mejorar significativamente la modularidad y la reutilización de nuestro código. A medida que sigas avanzando en tu aprendizaje de Python, encontrarás que muchos frameworks y bibliotecas (como Flask, Django o incluso las propias bibliotecas estándar de Python) hacen un uso extensivo de decoradores para proporcionar funcionalidades de manera elegante y mantenible.