Publicado el: 2019-11-22
Los decoradores en Python son una gran herramienta que hacen que el código sea más fácil de mantener y que desgraciadamente no se le suele dar tanta importancia. Son la manera en la cual puedes reutilizar un mismo código para alterar el funcionamiento de funciones cuando este se vuelve muy repetitivo.
Si lo quieres ver de esta forma, los decoradores son a las funciones lo que las interfaces son a los objetos. O si vienes de un ambiente mas rubyista, los decoradores son algo parecido a los bloques.
Los decoradores son básicamente la "trifecta" de las funciones ya que:
Los habrás visto y/o usado seguramente si haz trabajado alguna vez con el framework web Flask ya que es la manera recomendada que tiene este framework para crear rutas para tu servicio web.
=> Flask
@app.route("/") def index(): pass
@decorador(params) ^ ╚════ Caractér para indicar que decoraremos una función
La manera mas sencilla para detectar que una función está siendo "decorada" es por que justo arriba de su cabecera se encuentra algo parecido a una llamada a una función pero que comienza con @
.
@decorador(params) ^ ^ ^ ^ ^ ╚═╩═╩═╩═╩══ Nombre del decorador
Esto es bastante simple y no varía en lo absoluto con una llamada típica a una función... para saber cual decorador estamos usando, se usará su nombre.
@decorador(params) ^ ^ ^ ╚═╩═╩══ Parámetros para modificar el decorador (opcional)
Tampoco esto varia demasiado con las funciones de toda la vida. Si el decorador acepta parámetros, se le pasan colocándolos dentro de unos paréntesis.
@decorador(params) def index(): ^ ^ ^ ^ ^ ^ ╚═╩═╩═╩═╩═╩══ Función a decorar
Inmediatamente debajo del decorador se debe colocar la función que será decorada.
Como mencioné al inicio del artículo, los decoradores no son otra cosa más que simples funciones.
def decorador(f): def wrap(*args, **kwargs): return f(*args, **kwargs) return wrap @decorador def funcion(x): return x**2
Esta es el decorador mas simple que se puede escribir... YYYYY no hace absolutamente nada, pero nos puede dar una idea de como están compuestos.
def decorador(f): ^ ^ ^ ^ ^ ╚═╩═╩═╩═╩══ Nombre del decorador
Es el nombre que recibirá el decorador y que luego utilizaremos con la sintaxis de @nombre
.
def decorador(f): ^ ║ ╚═╦═══ Función a decorar siendo @decorador ║ capturada por el decorador def funcion(x):<╝
Este parámetro que está recibiendo nuestra función/decorador es una referencia a la función que posteriormente se colocará debajo del decorador.
def decorador(f): def wrap(*args, **kwargs): ^ ^ ╚═╩════ Nombre provisional para uso interno del decorador
Esta será la función que retornaremos, wrap
es solo un nombre provisional que usaremos unicamente dentro del decorador y que jamás se podrá ver al momento de utilizar el decorador.
def decorador(f): def wrap(*args, **kwargs): ^ ^ ^ ^ ^ ^ ^ ^ ╚═╩═╩═╩═╩═╩═╩═╩══ Nuevos argumentos que aceptará nuestra función
Como nosotros al momento de crear el decorador desconocemos la cantidad real de argumentos que aceptará la función decorada, simplemente debemos capturar absolutamente todos para luego "desenvolvernos" al momento de llamar a la función a decorar.
def decorador(f): def wrap(*args, **kwargs): return f(*args, **kwargs) ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ╚═╩═╩═╩═╩═╩═╩═╩═╩═╩═╩═╩═╩═══ Llamada a la función decorada desenvolviendo los parámetros capturados en el wrap
Recordemos que en f
se encuentra una referencia a la función a la cual queremos decorar, eso significa que si mandamos llamar a la función f
realmente estaremos llamando a la función que colocamos debajo de nuestro decorador, y como esto es un decorador simple, no hacemos otra cosa mas que retornar los que la función decorada nos retorne.
def decorador(f): def wrap(*args, **kwargs): return f(*args, **kwargs) return wrap ^ ^ ^ ^ ^ ^ ╚═╩═╩═╩═╩═╩══ Se retorna una referencia a nuestra función provisional
Aquí está el truco de todo esto ya que, como dije al principio, nuestro decorador está retornando una función, o mas bien, una referencia a nuestra función... así que cuando estamos llamando a una función decorada, realmente estamos mandando llamar a un impostor, a una función intermediaría entre nosotros y nuestra hermosa función.
def decorador(f): def wrap(*args, **kwargs): return f(*args, **kwargs) return wrap @decorador def funcion(x): return x**2
y boilá, nuestro primer decorador está listo para ser coronar a nuestras funciones y otorgarle cualidades especiales... ...o en este caso, ninguna cualidad.
Pero si creíste que el viaje por este maravilloso mundo de decoradores, funciones retornando funciones e impostores había acabo, pues estás equivocado ya que ahora subiremos al siguiente nivel, hacer que nuestro decorador admita * Tambores dramáticos * parámetros. Los decoradores que reciben parámetros no son muy distintos a los decoradores que NO reciben parámetros, solo que con una pequeña diferencia. Si los decoradores que NO reciben parámetros son una función que retorna una función, los decoradores que SI reciben parámetros son funciones que retornan una función que retorna una función * emoji impactado *. Pero esto, por que es así? por que nosotros al estar pasándole parámetros a una función realmente estamos llamando a una función y como a nosotros lo que nos interesa es otra función, pues la función a la que llamamos nos debe retornar otra... Yo se que te estoy confundiendo mas de lo que te estoy ayudando, así que mejor vamos a ver un ejemplo.
def decorador(*args, **kwargs): def decorator(f): def wrap(*args, **kwargs): return f(*args, **kwargs) return wrap return decorator
Aquí notamos algo familiar, no? def decorator
que recibe una función en su parámetro, un función cuyo nombres es wrap
que es retornado, pues si. En pocas palabras, podríamos decir que un decorador que SI recibe parámetros es un generador de decoradores que NO reciben parámetros. Genial, no? vamos a explicarlo un poco.
def decorador(*args, **kwargs): ^ ^ ^ ^ ^ ╚═╩═╩═╩═╩═══ Nombre de nuestro decorador
Comenzamos nuevo, este será el nombre que usaremos en la sintaxis de @decorador
con la diferencia de que ahora nuestro decorador no recibe una función en sus parámetros, sino que recibe sus propios parámetros. Aquí es donde el decorador que flask que vimos al principio tiene declarados los argumentos como la ruta o los métodos. Pueden ser recibimos como argumentos envueltos con *args, **kwargs
o podemos recibirlo uno a uno como una función cualquiera, por ejemplo:
def decorador(arg1, args2="default value", arg3=None)
y cuanta fantasía se nos vaya ocurriendo durante el proceso de programación. La otra parte del decorador es exactamente la misma que con los decoradores sin parámetros.
Como se puede apreciar, los decoradores no son mas que un juego entre recibir funciones y retornar funciones y esto se puede explotar hasta donde nuestra imaginación nos de. Pero la pregunta que quizá alguno de ustedes tengan es "¿Y eso que utilidad tiene?".
Como pudimos ver en los 2 bloques anteriores, nosotros al crear un decorador tenemos total acceso tanto a las funciones, como a los argumentos que estas están recibiendo, lo cual puede dar juego a hacer toda clase de validaciones y registros.
A lo largo de mi vida como programador en Python he escrito bastantes decoradores y quiero compartir (y explicar) con ustedes un par de ellos.
def try_catch(value=...): def decorator(f): def wrap(*args, **kwargs): try: return f(*args, **kwargs) except BaseException as e: if value is ...: return e else: return value return wrap return decorator @try_catch(0) def pow(num, exp): return num ** exp > pow(10, 2) 100 > pow(5, 5) 3125 > pow("foo", 10) 0 @try_catch() def pow(num, exp): return num ** exp > pow("foo", 10) TypeError("unsupported operand type(s) for ** or pow(): 'str' and 'int'")
Como se puede apreciar, este es un decorador que si recibe parámetros, para ser correcto, recibe un solo parámetro y es opcional y el característica que le otorga a las funciones es la de evitar que arrojen una excepción y si lo hace, retornar el valor que se le pasó al decorador como parámetro y si no se le pasa ningún parámetro, retornará (mas no lanzará) la excepción que arrojó la función.
def only_numbers(f): def wrap(*args, **kwargs): for arg in args: if not isinstance(arg, (int, float)): raise ValueError(f"{arg} is not an int or a float, is a {type(arg).__name__}") return f(*args, **kwargs) return wrap @only_numbers def suma(*args): return sum(args) > suma(1, 2, 3, 4, 5) 15 > suma(1, 2, 3, 4, "foo") --------------------------------------------------------------------------- ValueError Traceback (most recent call last) ... ValueError: a is not a int or a float, is a str
Este otro decorador no recibe parámetros y lo que hace es verificar que todos los parámetros que esté recibiendo nuestra función sean tipo int
o tipo float
, de lo contrarío arrojará una excepción advirtiendo que uno de los argumentos incumple esta regla.
Una ultima cosa antes de terminar y como dato que hará que les vuele cabeza.
Los decoradores también se pueden apilar al momento de ser utilizados decorando una función dando como resultado una herramienta verdaderamente poderosa para la reutilización de código. Los decoradores que puse de ejemplo no fueron al azar, pues los escogí exactamente por que se pueden apilar ya que uno es capaz de capturar excepciones y otro arroja una excepción, dando como resultado algo como:
@try_catch(-1) @only_numbers def suma(*args): return sum(args) > suma(1, 2, 3) 6 > suma(1, 2, "foo") -1
Muchas gracias por leer el primer post de mi blog, espero que les haya gustado. en mi twitter @kirbylife pueden hacerme llegar las opiniones con respecto al post, que les pareció, en que puedo mejorar o si tengo algún error, tanto en el código como en la redacción.
=> @kirbylife This content has been proxied by September (3851b).Proxy Information
text/gemini; charset=utf-8; lang=en