Modificando funciones en Python, los famosos decoradores

Decoradores

Los decoradores son funciones que modifican el comportamiento de otras funciones, ayudan a acortar nuestro código y hacen que sea más Pythonic. Si alguna vez has visto @, estás ante un decorador o decorator, bien sea uno que Python ofrece por defecto o uno que puede haber sido creado ex profeso.

Si aún no controlas las funciones te recomendamos que empieces con este post.

Veamos un ejemplo muy sencillo. Tenemos una función suma() que vamos a decorar usando mi_decorador(). Para ello, antes de la declaración de la función suma, hacemos uso de @mi_decorador.

def mi_decorador(funcion):
    def nueva_funcion(a, b):
        print("Se va a llamar")
        c = funcion(a, b)
        print("Se ha llamado")
        return c
    return nueva_funcion

@mi_decorador
def suma(a, b):
    print("Entra en funcion suma")
    return a + b

suma(5,8)

# Se va a llamar
# Entra en funcion suma
# Se ha llamado

Lo que realiza mi_decorador() es definir una nueva función que encapsula o envuelve la función que se pasa como entrada. Concretamente lo que hace es hace uso de dos print(), uno antes y otro después de la llamada la función.

Por lo tanto, cualquier función que use @mi_decorador tendrá dos print, uno y al principio y otro al final, dando igual lo que realmente haga la función.

Veamos otro ejemplo usando el decorador sobre otra función.

@mi_decorador
def resta(a ,b):
    print("Entra en funcion resta")
    return a - b

resta(5, 3)

# Se va a llamar
# Entra en funcion resta
# Se ha llamado

Una vez entendido esto, vamos a entrar un poco más en detalle viendo como Python trata a las funciones, como se puede definir un decorador sin @, como se pueden pasar argumentos a los decoradores, y para finalizar, un ejemplo práctico.

Definiendo decoradores

Antes de nada hay que entender que todo en Python es un objeto, incluso una función. De hecho se puede asignar una función a una variable. Nótese la diferencia entre:

  • di_hola() llama a la función.
  • di_hola hace referencia a la función, no la llama.
def di_hola():
    print("Hola")

f1 = di_hola() # Llama a la función
f2 = di_hola   # Asigna a f2 la función

print(f1)      # None, di_hola no devuelve nada
print(f2)      # <function di_hola at 0x1077bf158>

#f1()          # Error! No es válido
f2()           # Llama a f2, que es di_hola()

del f2         # Borra el objeto que es la función 
#f2()          # Error! Ya no existe

di_hola()      # Ok. Sigue existiendo

Entendido esto, demos un paso más. En Python se pueden definir funciones dentro de otras funciones. La función operaciones define suma() y resta(), y dependiendo del parámetro de entrada op, se devolverá una u otra.

def operaciones(op):
    def suma(a, b):
        return a + b
    def resta(a, b):
        return a - b

    if op == "suma":
        return suma
    elif op == "resta":
        return resta

funcion_suma = operaciones("suma")
print(funcion_suma(5, 7)) # 12

funcion_suma = operaciones("resta")
print(funcion_suma(5, 7)) # -2

Si llamamos a la función devuelta con dos operandos, se realizará una operación distinta en función de si se uso suma o resta.

Ahora ya podemos dar la última vuelta de tuerca y escribir nuestro propio decorador sin hacer uso de @. Por un lado tenemos el decorador, que recibe como entrada una función y devuelve otra función decorada. Por el otro la función suma() que queremos decorar.

def decorador(func):
    def envoltorio_func(a, b):
        print("Decorador antes de llamar a la función")
        c = func(a, b)
        print("Decorador después de llamar a la función")
        return c
    return envoltorio_func

def suma(a, b):
    print("Dentro de suma")
    return a + b

# Nueva funcion decorada
funcion_decorada = decorador(suma)

funcion_decorada(5, 8)

Entonces, haciendo uso de decorador y pasando como entrada suma, recibimos una nueva función decorada con una funcionalidad nueva, lista para ser usada. Sería el equivalente al uso de @.

Decoradores con parámetros

Tal vez quieras que tu decorador tenga algún parámetro de entrada para modificar su comportamiento. Se puede hacer envolviendo una vez más la función como se muestra a continuación.

def mi_decorador(arg):
    def decorador_real(funcion):
        def nueva_funcion(a, b):
            print(arg)
            c = funcion(a, b)
            print(arg)
            return c
        return nueva_funcion
    return decorador_real

@mi_decorador("Imprimer esto antes y después")
def suma(a, b):
    print("Entra en funcion suma")
    return a + b

suma(5,8)
# Imprimer esto antes y después
# Entra en funcion suma
# Imprimer esto antes y después

Es importante recalcar que los ejemplos mostrados hasta ahora son didácticos, y no tienen mucha utilidad práctica. Veamos un ejemplo más práctico.

Ejemplo: logger

Una de las utilidades más usadas de los decoradores son los logger. Su uso nos permite escribir en un fichero los resultados de ciertas operaciones, que funciones han sido llamadas, o cualquier información que en un futuro resulte útil para ver que ha pasado.

En el siguiente ejemplo tenemos un uso más completo del decorador log() que escribe en un fichero los resultados de las funciones que han sido llamadas.

def log(fichero_log):
    def decorador_log(func):
        def decorador_funcion(*args, **kwargs):
            with open(fichero_log, 'a') as opened_file:
                output = func(*args, **kwargs)
                opened_file.write(f"{output}\n")
        return decorador_funcion
    return decorador_log

@log('ficherosalida.log')
def suma(a, b):
    return a + b

@log('ficherosalida.log')
def resta(a, b):
    return a - b

@log('ficherosalida.log')
def multiplicadivide(a, b, c):
    return a*b/c

suma(10, 30)
resta(7, 23)
multiplicadivide(5, 10, 2)

Nótese que el decorador puede ser usado sobre funciones que tienen diferente número de parámetros de entrada, y su funcionalidad será siempre la misma. Escribir en el fichero pasado como parámetro los resultados de las operaciones.

Ejemplo: Uso autorizado

Otro caso de uso muy importante y ampliamente usado en Flask, que es un framework de desarrollo web, es el uso de decoradores para asegurarse de que una función es llamada cuando el usuario se ha autenticado.

Realizando alguna simplificación, podríamos tener un decorador que requiriera que una variable autenticado fuera True. La función se ejecutará solo si dicha variable global es verdadera, y se dará un error de lo contrario.

autenticado = True

def requiere_autenticación(f):
    def funcion_decorada(*args, **kwargs):
        if not autenticado:
            print("Error. El usuario no se ha autenticado")
        else:
            return f(*args, **kwargs)
    return funcion_decorada

@requiere_autenticación
def di_hola():
    print("Hola")

di_hola()

Prueba a cambiar la variable de True a False.

Y esto es todo. En otros posts veremos el uso de functools y porque nos puede ser muy útil.

¡Deja un comentario!

avatar
  Subscribe  
Notify of