Figura 1: Hyrum’s Law

En Python tenemos un concepto bastante interesante que es first class object, este concepto básicamente dice que todo en Python es un objeto, el cual este objeto tiene atributos y comportamientos particulares, esto tambien incluye a las funciones,  lo que quiere decir que en Python una función puede comportarse exactamente como una variable, veamos un ejemplo

def create_service(service: str):
    pass

print(dir(create_service))
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init_

Como se aprecia en el ejemplo, cree una función con el statement def llamada create_service y luego imprimí los atributos que se le agregan a esta función sin ejecutarla, eso es equivalente a construir una clase, crear un objeto e imprimir sus atributos,  de hecho si se ejecuta un type de la función, se obtiene que es una objeto de tipo function, esto ofrece bastante benéficos y da bastante 'tela por cortar',  por eso este post será bastante introductorio y lo enfocaremos un poco a los beneficios de este concepto

Evitando usar IF

Un beneficio bastante amplio es el de evitar un poco la gran cantidad de if anidados,  podríamos poner un ejemplo de contexto, en donde dependiendo de cierto parámetro se ejecuta un código, veamos como se vería


def service_automation():
    pass


def service_webhook():
    pass


def service_search():
    pass


def create_service(service: str):
    if service == "AUTOMATION":
        service_automation()
    elif service == "WEBHOOK":
        service_webhook()
    elif service == "SEARCH":
        service_search()

tener muchos ifs acoplados, puede generar una alarma, una opción es utilizando un dict, permitir acceder a estar función y ejecutarla, haciendo énfasis en que además el costo de utilizar un dict o hashmap siempre es bastante económico a nivel computacional


def service_automation():
    print("a lot of programming logic AUTOMATION")


def service_webhook():
    print("a lot of programming logic WEBHOOK")


def service_search():
    print("a lot of programming logic SEARCH")


def create_service(service: str):
    my_services = {
        "WEBHOOK":service_webhook,
        "AUTOMATION":service_automation,
        "SEARCH":service_search
    }
    service_to_execute = my_services.get(service, None)
    if service_to_execute:
        service_to_execute()
    else:
        print("Not found")



create_service("WEBHOOK")

¿Cómo ocurre esto? Bueno, como explique al principio, una función puede ser tratada como una variable, es decir, dentro de este dict se guarda una dirección de memoria donde está esa función, cuando la traigo, es decir, cuando llamo a .get() obtengo esa dirección y puedo ejecutarla sin problema, este ejemplo es bastante básico, pero ciertamente es la razón y base de una parte del uso de inyección de dependencias , y esto permite también desacoplar código que tal vez contenga muchos if, claramente la opción de mejora también puede ir enfocada a implementar algunos patrones de diseño

Funciones como parámetros

Las funciones en Python, al comportarse como variables, también pueden ser usadas como parámetros, esto para los que vienen de programación funcional le sonara un poco conocido, si es composition, la composición matemáticamente  es como, la Figura 2, el objetivo es combinar dos funciones generando una nueva función que ejecute las dos… En código nos podriamos entender mejor

Figura 2: Definición de composition

cambiemos un poco el ejemplo anterior, imaginemos que ya no necesitamos el diccionario, sino que tenemos la función ya escrita, debemos registrar la respuesta de esa función  que ejecutada por algún framework o request, pero sin modificar el código que ya está, esto puede ocurrir debido a que los requerimientos pueden no estar relacionados con la lógica de negocio y podría 'ensuciar' un poco la legibilidad

from typing import Callable, Dict


def service_automation(*args, **kwargs) -> str:
    code = kwargs.get('code', '')
    return f"a lot of programming logic AUTOMATION {code}"


def service_webhook(*args, **kwargs) -> str:
    code = kwargs.get('code', '')
    return f"a lot of programming logic WEBHOOK {code}"


def service_search(*args, **kwargs) -> str:
    code = kwargs.get('code', '')
    return f"a lot of programming logic SEARCH {code}"


def register_metric(response:str):
    print(f'Uploaded data {response}')


def create_service(service_function: Callable[[Dict], str], data) -> str:
    response = service_function(**data)
    register_metric(response)
    return response


print(create_service(service_search, {"code":"test"}))
Uploaded data a lot of programming logic SEARCH test
a lot of programming logic SEARCH test

Bien para explicar un poco el código, inicialmente a las funciones agregamos dos valores *args y **kwargs , Python permite pasar parámetros de forma posicional, es decir  invocar una función y pasar los valores en el orden que se reciben y además permite pasar parámetros utilizando un keyword que es el nombre de la variable, este indica que todas las variables argumentales las guarde en args como una tupla, y los parámetros con nombre de variable los guarde dentro de un dict llamado kwargs, esto se puede realizar para ser flexibles con la cantidad de parámetros que se van a recibir, en este video lo explican muy bien luego de ello create_service recibe una función, la ejecuta y le pasa la variable data, registra la metrica y retorna la respuesta de la ejecución, es decir que cuando se invoque create_service() realmente se está invocando service function fíjense también en los typing, esto es muy importante pues a medida que la complejidad de las funciones aumento esto le agrega legalidad, él  se lee asi Callable[[tipo dato parametro], tipo dato respuesta] el tipo dato parámetro es una lista, porque pueden ser muchos tipos de datos de entrada, de esta forma se logra ejecutar una funcion dentro de otra, ahora python tiene una sintaxis especial, llamada decoradores

Decoradores

Con el concepto claro de composition, ahora podemos tratar de entender un decorador, un decorador básicamente es una función que acepta una función, es una 'syntactic sugar' de python para aplicar composition, también entra el concepto de closure que podríamos adentrar en otro post más a profundidad, veamos que significa esto, en el ejemplo anterior funciona, pero no es muy usable, pues tener que escribir una función y enviarle a la función a otra es un poco invasivo con el código y además no es están limpio, por eso los decoradores básicamente ejecutan la función y la devuelven, haciendo que con un prefijo la función en código no cambie, en ese sentido da un valor inmenso porque prácticamente es posible modificar el comportamiento de una función, sin cambiar el código, veamos como seria

from typing import Callable, Dict


def register_metric(response:str):
    print(f'Uploaded data {response}')


def create_service(service_function: Callable[[Dict], str]) -> Callable[[Dict], str]:
    def wrapper(data: dict):
        response = service_function(**data)
        register_metric(response)
        return response

    return wrapper


@create_service
def service_automation(*args, **kwargs) -> str:
    code = kwargs.get('code', '')
    return f"a lot of programming logic AUTOMATION {code}"


@create_service
def service_webhook(*args, **kwargs) -> str:
    code = kwargs.get('code', '')
    return f"a lot of programming logic WEBHOOK {code}"


@create_service
def service_search(*args, **kwargs) -> str:
    code = kwargs.get('code', '')
    return f"a lot of programming logic SEARCH {code}"


print(service_search({"code":"test"}))
Uploaded data a lot of programming logic SEARCH test
a lot of programming logic SEARCH test

Este ejemplo tiene algunos cambios, para entenderlos

  • Un decorador no necesita una sintaxis para poder implementarlo, es solamente una función que reciba una función
  • Cuando se importa la función se utiliza @function_name sobre algún método o función ya se puede usar el decorador

este ejemplo es exactamente igual al que teníamos, solamente que ahora se retorna una función, esta función que retorna reemplaza la función a la que se le agrega el decorador, pero esto sucede con una sintaxis de prefijo, por eso no hay una invocación explícita de la función create_service, pero si es una invocación implícita que hace que esta función service_search para el ejemplo se reemplace, permitiendo  modificar una entrada o una salida de una función  escrita, pero sin modificar su código propiamente

Conclusiones

Los decoradores tiene una gran utilidad dentro de python, de hecho muchas librerías como django, FastAPI y Flask lo usan porque permiten modificar, agregarle, quitarle y modificar la funciónque los usan, transformando estos decoradores en código reutilizable, pues este decorador se puede implementar a cualquier función o método, para lograr entenderlo es importante tener claro algunos conceptos de programación funcional, por ello dejaré este libro el cual está bastante interesante

Únete al equipo de Simetrik AQUI