Cómo Usar Decoradores en Python: Guía Completa para Principiantes
Los decoradores en Python te permiten agregar funcionalidades a tus métodos sin modificar su código original y mucho más. Vamos a sumergirnos en este fascinante tema y descubrir cómo puedes utilizarlos en tu propio código.
¿Qué son los Decoradores en Python?
Los decoradores en Python son una herramienta poderosa que permite modificar el comportamiento de una función o método. En términos simples, un decorador es una función que toma otra función y extiende su comportamiento sin modificar su estructura interna.
La documentación oficial de python nos dice:
La sintaxis del decorador es meramente azúcar sintáctico, las definiciones de las siguientes dos funciones son semánticamente equivalentes:
def f(arg):
...
f = staticmethod(f)
@staticmethod
def f(arg):
...
Ejemplo Básico de Decorador:
Para entender mejor, veamos un ejemplo simple. Imagina que quieres crear un decorador que imprima un mensaje antes y después de ejecutar una función.
def mi_decorador(func): # funcion que acepta una función com argumento. Este es el decorador
def nueva_funcion(*args, **kwargs): # esta funcion reemplazara la funcion original `func`
print("Antes de ejecutar la función")
resultado = func(*args, **kwargs) # la función func se llama con sus argumentos originales
print("Después de ejecutar la función")
return resultado
return nueva_funcion
@mi_decorador
def di_hola():
print("Hola, Mundo!")
di_hola()
Al ejecutar di_hola(), la salida será:
Antes de ejecutar la función
Hola, Mundo!
Después de ejecutar la función
¿Por qué Usar Decoradores?
1. Reutilización de Código
Los decoradores permiten reutilizar código fácilmente. Puedes aplicar el mismo decorador a múltiples funciones, asegurando que todas compartan un comportamiento común.
2. Separación de Preocupaciones
Ayudan a mantener tu código limpio y ordenado al separar la lógica principal de la función de los aspectos transversales como la validación, registro de logs o manejo de excepciones.
3. Mejora de la Legibilidad
Al utilizar decoradores, mejoras la legibilidad y mantenibilidad de tu código. Es más fácil entender qué hace una función cuando las responsabilidades adicionales están claramente definidas en decoradores separados.
Cómo Crear y Utilizar Decoradores
Crear un decorador implica definir una función que envuelve otra función. Vamos a desglosar el proceso paso a paso.
Paso 1: Definir el Decorador
Primero, definimos el decorador como una función que toma otra función como argumento.
def mi_decorador(func):
def envoltura():
# Código antes de llamar a la función
func()
# Código después de llamar a la función
return envoltura
Paso 2: Aplicar el Decorador
Utiliza el símbolo @ seguido del nombre del decorador antes de la función que deseas decorar.
@mi_decorador
def mi_funcion():
print("Esta es mi función")
Cuando se llama a mi_function()
, en realidad se ejecuta la función envontura
definida en el decorador, que a su vez ejecuta mi_function
con el comportamiento adicional.
Ejemplo completo:
def mi_decorador(func):
def envoltura():
print("Antes de llamar a la función")
func()
print("Después de llamar a la función")
return envoltura
@mi_decorador
def mi_funcion():
print("Esta es mi función")
mi_funcion()
Salida del Ejemplo Completo:
Antes de llamar a la función
Esta es mi función
Después de llamar a la función
Decoradores Anidados
Los decoradores también pueden anidarse, lo que significa que puedes aplicar más de un decorador a una función. Esto te permite combinar múltiples funcionalidaddes en una sola función.
def decorador_1(func):
def envoltura():
print("Antes de llamar al decorador 1")
func()
print("Después de llamar al decorador 1")
return envoltura
def decorador_2(func):
def envoltura():
print("Antes de llamar al decorador 2")
func()
print("Después de llamar al decorador 2")
return envoltura
@decorador_1
@decorador_2
def mi_funcion():
print("Esta es mi función")
mi_funcion()
Cuando se llama a mi_funcion()
, se ejecutan ambos decoradores en orden. La salida será:
Antes de llamar al decorador 1
Antes de llamar al decorador 2
Esta es mi función
Después de llamar al decorador 2
Después de llamar al decorador 1
Decoradores con Argumentos
También puedes crear decoradores que acepten argumentos. Esto agrega una capa adicional de flexibilidad.
def decorador_repetidor(numero_de_veces):
def envoltura(func):
def nueva_funcion(*args, **kwargs):
for _ in range(numero_de_veces):
func(*args, **kwargs)
return nueva_funcion
return envoltura
@decorador_repetidor(3)
def di_adios():
print("Adiós, Mundo!")
di_adios()
Este decorador ejecutará la función di_adios tres veces.
Decoradores con Argumentos Opcionales
Para hacer que los argumentos del decorador sean opcionales, puedes utilizar una función de fábrica que devuelva el decorador real.
def decorador_repetidor(numero_de_veces=1):
def envoltura(func):
def nueva_funcion(*args, **kwargs):
for _ in range(numero_de_veces):
func(*args, **kwargs)
return nueva_funcion
return envoltura
@decorador_repetidor()
def di_hola():
print("Hola, Mundo!")
@decorador_repetidor(3)
def di_adios():
print("Adiós, Mundo!")
di_hola()
di_adios()
En este caso, el decorador_repetidor() sin argumentos ejecutará la función una vez, mientras que decorador_repetidor(3) la ejecutará tres veces.
Hola, Mundo!
Adiós, Mundo!
Adiós, Mundo!
Adiós, Mundo!
Decoradores con Argumentos Arbitrarios
Si deseas aplicar un decorador con argumentos arbitrarios, puedes utilizar *args
y **kwargs
para capturar cualquier número de argumentos.
def decorador_con_argumentos(func):
def envoltura(*args, **kwargs):
print("Argumentos posicionales:", args)
print("Argumentos de palabras clave:", kwargs)
func(*args, **kwargs)
return envoltura
@decorador_con_argumentos
def mi_funcion(*args, **kwargs):
print("Esta es mi función")
mi_funcion(1, 2, 3, nombre="Alice", edad=30)
Al llamar a mi_funcion(1, 2, 3, nombre="Alice", edad=30)
, obtendrás la siguiente salida:
Argumentos posicionales: (1, 2, 3)
Argumentos de palabras clave: {'nombre': 'Alice', 'edad': 30}
Esta es mi función
Decoradores en Clases
Los decoradores también se pueden aplicar a métodos dentro de clases.
Veamos un ejemplo sencillo:
def mi_decorador_metodo(func):
def nueva_funcion(self, *args, **kwargs):
print("Método decorado")
return func(self, *args, **kwargs)
return nueva_funcion
class MiClase:
@mi_decorador_metodo
def mi_metodo(self):
print("Este es un método de clase")
obj = MiClase()
obj.mi_metodo()
Decoradores de Clase
Además de decoradores de funciones, Python también permite decoradores de clase. Un decorador de clase se aplica a una clase y puede modificar su comportamiento.
# Definición del decorador de clase
def decorador_clase(cls):
class NuevaClase:
def __init__(self, *args, **kwargs):
self.instancia = cls(*args, **kwargs)
def __getattr__(self, nombre):
return getattr(self.instancia, nombre)
def metodo_adicional(self):
print("Este es un método adicional añadido por el decorador")
return NuevaClase
# Uso del decorador en una clase
@decorador_clase
class MiClase:
def __init__(self, valor):
self.valor = valor
def mostrar_valor(self):
print(f"Valor: {self.valor}")
# Crear una instancia de la clase decorada
obj = MiClase(10)
obj.mostrar_valor() # Llama al método original de MiClase
obj.metodo_adicional() # Llama al nuevo método añadido por el decorador
Decoradores con Argumentos de Clase
También puedes crear decoradores que acepten argumentos de clase. Para ello, debes definir una clase que actúe como decorador y tenga un método __call__()
, este método es un método especial en Python que permite que una instancia de la clase se comporte como una función. Es decir, permite que un objeto sea “llamable”, como si fuera una función.
class DecoradorConArgumentosDeClase:
def __init__(self, argumento):
self.argumento = argumento
def __call__(self, func):
def envoltura(*args, **kwargs):
print(f"Este es el argumento de clase: {self.argumento}")
func(*args, **kwargs)
return envoltura
@DecoradorConArgumentosDeClase("decorador")
def mi_funcion():
print("Esta es mi función")
mi_funcion()
Al llamar a mi_funcion()
, obtendrás la siguiente salida:
Este es el argumento de clase: decorador
Esta es mi función
Decoradores con Estado
A veces, es útil que un decorador mantenga un estado interno entre llamadas. Puedes lograr esto utilizando una clase en lugar de una función para definir el decorador.
class DecoradorContador:
def __init__(self, func):
self.func = func
self.contador = 0
def __call__(self, *args, **kwargs):
self.contador += 1
print(f"Esta función ha sido llamada {self.contador} veces")
return self.func(*args, **kwargs)
@DecoradorContador
def mi_funcion():
print("Esta es mi función")
mi_funcion()
mi_funcion()
En este ejemplo, el decorador DecoradorContador mantiene un contador interno que se incrementa cada vez que se llama a la función decorada. La salida será:
Esta función ha sido llamada 1 veces
Esta es mi función
Esta función ha sido llamada 2 veces
Esta es mi función
Decoradores Predefinidos en Python
Python proporciona algunos decoradores predefinidos que se pueden utilizar para realizar tareas comunes. Algunos de los decoradores más comunes son:
- @staticmethod: Convierte un método en un método estático.
- @classmethod: Convierte un método en un método de clase.
- @property: Convierte un método en un atributo de solo lectura.
- @lru_cache: Almacena en caché los resultados de una función para evitar cálculos redundantes.
- @functools.wraps: Preserva la información de metadatos de una función original cuando se aplica un decorador.
Uso de @staticmethod y @classmethod:
class MiClase:
def __init__(self, valor):
self._valor = valor
@staticmethod
def metodo_estatico():
print("Este es un método estático")
@classmethod
def metodo_de_clase(cls):
print("Este es un método de clase")
obj = MiClase(42)
obj.metodo_estatico() # Llamada al método estático a través de una instancia
MiClase.metodo_estatico() # Llamada al método estático directamente desde la clase
obj.metodo_de_clase() # Llamada al método de clase a través de una instancia
MiClase.metodo_de_clase() # Llamada al método de clase directamente desde la clase
En este ejemplo, metodo_estatico
es un método estático que se puede llamar sin crear una instancia de la clase, mientras que metodo_de_clase
es un método de clase que recibe la clase como argumento.
Uso de @property
El decorador @property permite definir métodos que pueden ser accedidos como atributos. También se puede usar junto con @setter y @deleter para definir el comportamiento de establecimiento y eliminación de propiedades.
class MiClase:
def __init__(self, valor):
self._valor = valor
@property
def valor(self):
return self._valor
@valor.setter
def valor(self, nuevo_valor):
if nuevo_valor >= 0:
self._valor = nuevo_valor
else:
raise ValueError("El valor debe ser mayor o igual a 0")
@valor.deleter
def valor(self):
del self._valor
# Crear una instancia de la clase
obj = MiClase(10)
print(obj.valor) # Output: 10
obj.valor = 20 # Utiliza el setter
print(obj.valor) # Output: 20
del obj.valor # Utiliza el deleter
Uso de @lru_cache
El decorador @lru_cache se utiliza para almacenar en caché los resultados de una función, evitando cálculos redundantes.
from functools import lru_cache
@lru_cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10)) # Output: 55
Uso de @wraps
El decorador @wraps se utiliza para preservar la información de metadatos de una función original cuando se aplica un decorador.
from functools import wraps
def mi_decorador(func):
@wraps(func)
def envoltura(*args, **kwargs):
print("Antes de llamar a la función")
resultado = func(*args, **kwargs)
print("Después de llamar a la función")
return resultado
return envoltura
@mi_decorador
def mi_funcion():
"""Esta es una función de ejemplo"""
print("Esta es mi función")
print(mi_funcion.__name__) # Output: mi_funcion
print(mi_funcion.__doc__) # Output: Esta es una función de ejemplo
Conclusión
Los decoradores en Python son una herramienta increíblemente útil para cualquier desarrollador. No solo te permiten añadir funcionalidades de manera flexible y ordenada, sino que también mejoran la legibilidad y mantenibilidad de tu código. Al entender y aplicar los decoradores, podrás escribir código más eficiente y limpio.