En Python, un decorador es una construcción especial que nos permite añadir funcionalidad extra a una función o clase existente sin modificar su código fuente.
Un decorador es un callable que toma otra función o clase como entrada y devuelve una versión modificada de la misma.
Empecemos con algo que ya hemos visto relacionado con funciones de primera clase, first class functions, i.e., tratar funciones como cualquier otro objeto.
def outer_f():
mensaje = "Hola"
def inner_f():
print(mensaje)
return inner_f()
# Si ejecutamos outer, se ejecuta
# función inner
outer_f()
Closures nos daban la ventaja de crear "instancias" de funciones, i.e., llevarnos las variables libres en el entorno de la función en la que fueron creadas una vez que estas funciones se agotaron porque ya fueron ejecutadas.
# Anulo la ejecucion de la funcion
def outer_f():
mensaje = "Hola"
def inner_f():
print(mensaje)
return inner_f
# para ejecutarla, tengo que llamarla
outer_f()()
# Pongamos un parametro en la funcón
def outer_f(msg):
mensaje = msg
def inner_f():
print(mensaje)
return inner_f
# Tengo dos instancias de funciones
my_f_hola = outer_f("hola")
my_f_chau = outer_f("chau")
print(my_f_hola.__name__)
print(my_f_chau.__name__)
my_f_hola()
my_f_chau()
Podemos simplificar la función, ya que teniendo en cuenta el scope, no necesitamos definir mensaje
Es decir, la variable mensaje no aporta inicialmente nada a la función _innerf
# Eliminamos
def outer_f(msg):
def inner_f():
print(msg)
return inner_f
Utilicemos esto para introducir decorators
Un decorator es una función:
1.- Que toma a otra función como argumento
2.- Le añade funcionalidad
3.- Retorna otra funcíón
Todo esto sin alterar el código de la función original dada en 1.
Para verlo, reescribamos la definición de la función anterior en términos de un decorador.
````python def outer_f(msg): def inner_f(): print(msg) return inner_f
def decorator_function(msg):
def wrapper_function():
print(msg)
return wrapper_function
Recordemos lo que hace esta función
1.- Retorna un a función que espera que sea ejectuada: wrapper_function
2.- Esa función, cuando se ejectua, imprime el mensaje.
Ahora: por qué no cambiar el print
de un mensaje por la ejecución de una función que pasamos como argumento.
Esto es lo que hace un decorador: envuelvo una función orginal con otra función
def decorator_function(original_function):
def wrapper_function():
return original_function()
return wrapper_function
# Definamos la función original como
def display():
print("La función display se ha ejecutado")
decorated_display = decorator_function(display)
decorated_display.__name__
decorated_display()
Claro, no hemos hecho mas nada de lo que hemos visto en closures...agregemos algo de decoración
def decorator_function(original_function):
def wrapper_function():
print(f"Estamos decorando la función: {original_function.__name__}")
return original_function()
return wrapper_function
decorated_display = decorator_function(display)
decorated_display()
Un decorador nos permite rapidamente añadir funcinalidad a una función.
Sintaxis:
@decorator_function
def display():
print("La función display se ha ejecutado")
# Ahora basta llamar la función original
display()
Que sucede si nuestra función original tiene inputs
@decorator_function
def suma(a,b):
return a + b
suma(4,5)
def decorator_function(original_function):
def wrapper_function(*args, **kwargs):
print(f"Estamos decorando la función: {original_function.__name__}")
return original_function(*args, **kwargs)
return wrapper_function
@decorator_function
def suma(a,b):
return a + b
suma(5,6)
Veamos un ejemplo con un gráfico
import matplotlib.pyplot as plt
import numpy as np
def overplot(func):
def plot_wrapper():
ax = plt.gca()
ax.set_xlabel("x")
ax.set_ylabel("y")
plt.title("Decorators")
func()
return plot_wrapper
@overplot
def plot_function():
x, y = np.linspace(0,5), np.linspace(0,5)
plt.plot(x,y)
plot_function()
plt.show()
Añadir flexibilidad: decorador con argumentos
Por ejemplo, si los labels los ejes cambian, o cambia el titulo.
Para ello, tenemos que añadir un nivel
def decorator_input(xlabel = " ", ylabel = " ", xytitle = " "):
def overplot(func):
def plot_wrapper():
ax = plt.gca()
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
plt.title(xytitle)
func()
return(plot_wrapper)
return overplot
@decorator_input(xlabel = "Las ordenadas ", ylabel = "Las absisas", xytitle = "Titulo")
def plot_function():
x, y = np.linspace(0,5), np.linspace(0,5)
plt.plot(x,y)
plt.show()
plot_function()
def decorate(**options):
"""Decora los axes.
Call decorate with keyword arguments like
decorate(title='Title',
xlabel='x',
ylabel='y')
The keyword arguments can be any of the axis properties
https://matplotlib.org/api/axes_api.html
"""
ax = plt.gca()
ax.set(**options)
handles, labels = ax.get_legend_handles_labels()
if handles:
ax.legend(handles, labels)
plt.tight_layout()