Seguramente habrás visto una sintaxis como esta:
app = Flask("app")
@app.route("/")
def index():
return "Hello World!"
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:
- La capacidad de ser pasados como argumentos a una función
- 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")
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ámetrofn
(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 llamadaenvoltura
cuyos argumentos son similares a la funciónsaludador
definida unas líneas arriba - La función
envoltura
ejecutafn
y le agrega "¡¡¡" y "!!!" al resultado, y lo retorna
- dentro de
- La variable
saludador_animado
es del tipo función, piénsalo, es el resultado de llamar aadmiracion
- 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")
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")
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
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")
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__)
La salida no es lo que esperamos...
envoltura
Just a cool function wrapper
¿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__)
Y ahora si, crisis resuelta:
python
Returns the name of the best programming language
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
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)
Executing the function related to the route /
Hello world!
Executing the function related to the route /about
This would be the about page?
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)