tacosdedatos

Cover image for Decoradores en Python
Antonio Feregrino
Antonio Feregrino

Posted on • Updated on

Decoradores en Python

Seguramente habrás visto una sintaxis como esta:

app = Flask("app")

@app.route("/")
def index():
    return "Hello World!"
Enter fullscreen mode Exit fullscreen mode

Pero, ¿te has detenido a pensar qué es lo que significa ese @ antes de app.route("/")? en este post te voy a contar de qué se trata.

Los decoradores

Ese fragmento de código es conocido como un decorador en Python. Los decoradores forman parte de un patrón de diseño, el cual permite al usuario añadirle funcionalidad a un objeto (método o función) sin modificar su estructura interna.

Funciones – ciudadanos de primera clase

Antes de seguir hablando sobre decoradores, debemos recordar que en Python, las funciones son elementos de "primera clase", lo cual significa que podemos tratarlas como cualquier otra variable. De entre todos esos derechos, los que más nos interesa son los de:

  1. La capacidad de ser pasados como argumentos a una función
  2. La capacidad de ser retornadas como resultado de una función
def saludador(saludado):
    return f"Hola {saludado}"

def admiracion(fn):

    def envoltura(persona):
        resultado = fn(persona)
        return f"¡¡¡ {resultado} !!!"

    return envoltura

saludador_animado = admiracion(saludador)

saludador_animado("Feregrino")
Enter fullscreen mode Exit fullscreen mode

Wow, wow, wow, hay un montón de cosas sucediendo en el fragmento anterior, vamos a desempacar lo que está sucediendo ahí:

  • Comenzando por saludador que es una función que recibe un parámetro y retorna una cadena de texto.
  • admiracion es otra función que recibe un parámetro fn (que debe ser otra función), y retorna otra función definida dentro de si misma.
    • dentro de admiracion se define todavía otra función llamada envoltura cuyos argumentos son similares a la función saludador definida unas líneas arriba
    • La función envoltura ejecuta fn y le agrega "¡¡¡" y "!!!" al resultado, y lo retorna
  • La variable saludador_animado es del tipo función, piénsalo, es el resultado de llamar a admiracion
  • Como saludador_animado es una función, podemos llamarla con un parámetro del tipo cadena y obtendremos el resultado de "¡¡¡ Hola Feregrino !!!"

Una mejor sintaxis

Si bien podemos tomarnos la tarea de crear funciones directamente nosotros, como la forma en la que creamos saludador_animado en nuestro ejemplo de arriba, esto puede volverse muy tedioso si contamos con muchas funciones y hay que hacer lo mismo para todas.

Para alivianarnos esa carga, podemos hacer uso de una sintaxis muy curiosa en Python, que logra el mismo efecto usando un poco de azúcar sintáctica. Hablo de la misteriosa @:

@admiracion
def despedidor(persona):
    return f"Adiós {persona}"

despedidor("Feregrino")
Enter fullscreen mode Exit fullscreen mode

Lo que veremos cuando ejecutamos la función despedidor es "¡¡¡ Adiós Feregrino !!!", porque estamos decorando la función con @admiracion para añadirle la funcionalidad extra.

El fragmento de código arriba es más o menos equivalente a hacer esto:

despedidor = admiracion(despedidor)

despedidor("Feregrino")
Enter fullscreen mode Exit fullscreen mode

Sí, es como "sobreescribir" la definición de la función, "envolviéndola" dentro de otra.

¿Cómo lo hace Flask?

Tal vez te estarás preguntando qué es lo que hace Flask, ¿por qué podemos crear un objeto y después usar uno de sus métodos como decorador? y la respuesta es: un método no es sino otra función. Mira el siguiente ejemplo de código, en donde definimos una clase con un método que luego podemos usar como decorador:

class Emocion:

    def __init__(self, apertura, cierre):
        self.apertura = apertura
        self.cierre = cierre

    def agrega_emocion(self, fn):

        def _envoltura(cadena):
            resultado = fn(cadena)
            return f"{self.apertura} {resultado} {self.cierre}"

        return _envoltura
Enter fullscreen mode Exit fullscreen mode

Una clase común y corriente, con un método que retorna una función... pero que podemos usar como un decorador:

emo = Emocion("¡¿", "?!")

@emo.agrega_emocion
def tardes(persona):
    return "Buenas tardes " + persona

tardes("Antonio")
Enter fullscreen mode Exit fullscreen mode

El resultado será "¡¿ Buenas tardes Antonio ?!".

Crisis de identidad de las funciones – wraps

En general, con eso debe bastarnos para crear decoradores, pero antes de cantar victoria, revisa el siguiente fragmento de código:

def is_cool(function):
    def envoltura():
        """Just a cool function wrapper"""
        return function() + " is cool!"
    return envoltura

@is_cool
def python():
    """Returns the name of the best programming language"""
    return "Python"

print(python.__name__)
print(python.__doc__)
Enter fullscreen mode Exit fullscreen mode

La salida no es lo que esperamos...

envoltura
Just a cool function wrapper
Enter fullscreen mode Exit fullscreen mode

¿La función python dice que se llama _wrapper? ¿la documentación de la función es en realidad la documentación de _wrapper? nuestra función está sufriendo una crisis de identidad y todo es culpa de nuestro decorador.

La solución es usar un decorador para decorar nuestra función envoltura, este decorador es llamado wraps y está disponible en el módulo functools:

from functools import wraps

def is_cool(function):

    @wraps(function)
    def envoltura():
        """Just a cool function wrapper"""
        return function() + " is cool!"
    return envoltura

@is_cool
def python():
    """Returns the name of the best programming language"""
    return "Python"

print(python.__name__)
print(python.__doc__)
Enter fullscreen mode Exit fullscreen mode

Y ahora si, crisis resuelta:

python
Returns the name of the best programming language
Enter fullscreen mode Exit fullscreen mode

No voy a discutir a detalle qué es lo que wraps hace, pero a grandes rasgos copia los metadatos de una función a la otra, esto con la idea de hacer el decorador lo más "transparente" posible.

Decoradores con argumentos

Si bien arriba tratamos de emular la sintaxis que usa Flask, te habrás dado cuenta de que hace falta una pieza importante: los argumentos del decorador. En Flask tenemos que escribir @app.route("/") no @app.route.

Resulta que usar un decorador que reciba otros argumentos, además de la función que queremos decorar es un poco más complicado. Vamos a tratar de replicar la API de Flask ahora si:

class FlaskClone:
    def __init__(self, app_name):
        self.app_name = app_name
        self.route_map = dict()

    def route(self, route_name):

        def _route_inner(function):

            def _actual_route_processor(web_request):
                # Dummy code
                print(f"Executing the function related to the route {route_name} within the app {self.app_name}")
                return function(web_request)

            self.route_map[route_name] = _actual_route_processor
            return _actual_route_processor

        return _route_inner 
Enter fullscreen mode Exit fullscreen mode

Antes de seguir con el post, vamos a ver el fragmento de código en acción:

app = FlaskClone("Cool app")

@app.route("/")
def home(web_request):
    print("Hello world!")

@app.route("/about")
def about(web_request):
    print("This would be the about page?")

app.route_map["/"](0)
app.route_map["/about"](0)
Enter fullscreen mode Exit fullscreen mode
Executing the function related to the route /
Hello world!
Executing the function related to the route /about
This would be the about page?
Enter fullscreen mode Exit fullscreen mode

Si prestas atención a la definición del método route te darás cuenta de que no es sino una extensión de lo que ya vimos anteriormente: una función que regresa otra función.

Conclusión

Puede parecer una complicada anidación de definición de funciones... y la verdad es que si, es por eso que no te encontrarás escribiendo decoradores en tu día a día, y es más, puede ser que pasen años sin que tengas que hacer uno.

La tarea de crear decoradores está relacionada principalmente con la creación de frameworks, cuando quieres crear una herramienta que otras personas puedan usar a través de una sintaxis limpia.

A pesar de que no tengas que estar escribiendo tus propios decoradores, me parece importante que sepas cómo se crean, y cómo es que están implementados internamente para saciar tu curiosidad.

Espero que este post te haya servido, y si así fue no dejes de compartirlo en internet, ah y me puedes seguir en Twitter si quieres saber más cosas sobre lo que publico.

Discussion (0)