Decoradores de pitón
Decoradores en Python
Python tiene una característica interesante llamada decoradores para agregar funcionalidad a un código existente.
Esto también se llama metaprogramación porque una parte del programa intenta modificar otra parte del programa en tiempo de compilación.
Requisitos previos para aprender decoradores
Para entender acerca de los decoradores, primero debemos saber algunas cosas básicas en Python.
Debemos sentirnos cómodos con el hecho de que todo en Python (¡Sí! Incluso las clases) son objetos. Los nombres que definimos son simplemente identificadores vinculados a estos objetos. Las funciones no son una excepción, también son objetos (con atributos). Se pueden vincular varios nombres diferentes al mismo objeto de función.
Aquí hay un ejemplo.
def first(msg):
print(msg)
first("Hello")
second = first
second("Hello")
Producción
Hello Hello
Cuando ejecuta el código, ambas funciones first
y second
dar la misma salida. Aquí, los nombres first
y second
se refieren al mismo objeto de función.
Ahora las cosas empiezan a ponerse más raras.
Las funciones se pueden pasar como argumentos a otra función.
Si ha utilizado funciones como map
, filter
y reduce
en Python, entonces ya sabes sobre esto.
Las funciones que toman otras funciones como argumentos también se llaman funciones de orden superior. Aquí hay un ejemplo de tal función.
def inc(x):
return x + 1
def dec(x):
return x - 1
def operate(func, x):
result = func(x)
return result
Invocamos la función de la siguiente manera.
>>> operate(inc,3)
4
>>> operate(dec,3)
2
Además, una función puede devolver otra función.
def is_called():
def is_returned():
print("Hello")
return is_returned
new = is_called()
# Outputs "Hello"
new()
Producción
Hello
Aquí, is_returned()
es una función anidada que se define y devuelve cada vez que llamamos is_called()
.
Finalmente, debemos saber sobre Closures en Python.
Volviendo a Decoradores
Las funciones y los métodos se llaman invocable como se les puede llamar.
De hecho, cualquier objeto que implemente el especial __call__()
método se denomina invocable. Entonces, en el sentido más básico, un decorador es un invocable que devuelve un invocable.
Básicamente, un decorador toma una función, agrega alguna funcionalidad y la devuelve.
def make_pretty(func):
def inner():
print("I got decorated")
func()
return inner
def ordinary():
print("I am ordinary")
Cuando ejecuta los siguientes códigos en shell,
>>> ordinary()
I am ordinary
>>> # let's decorate this ordinary function
>>> pretty = make_pretty(ordinary)
>>> pretty()
I got decorated
I am ordinary
En el ejemplo que se muestra arriba, make_pretty()
es decorador En el paso de asignación:
pretty = make_pretty(ordinary)
La función ordinary()
se decoró y la función devuelta recibió el nombre pretty
.
Podemos ver que la función de decorador agregó algunas funciones nuevas a la función original. Esto es similar a empacar un regalo. El decorador actúa como envoltorio. La naturaleza del objeto que se decoró (regalo real en el interior) no se altera. Pero ahora, se ve bonito (desde que se decoró).
Generalmente, decoramos una función y la reasignamos como,
ordinary = make_pretty(ordinary).
Esta es una construcción común y por esta razón, Python tiene una sintaxis para simplificar esto.
Podemos usar el @
símbolo junto con el nombre de la función decoradora y colóquelo encima de la definición de la función a decorar. Por ejemplo,
@make_pretty
def ordinary():
print("I am ordinary")
es equivalente a
def ordinary():
print("I am ordinary")
ordinary = make_pretty(ordinary)
Esto es solo un azúcar sintáctico para implementar decoradores.
Decorar funciones con parámetros
El decorador anterior era simple y solo funcionaba con funciones que no tenían ningún parámetro. ¿Qué pasaría si tuviéramos funciones que tomaran parámetros como:
def divide(a, b):
return a/b
Esta función tiene dos parámetros, a y b. Sabemos que dará error si pasamos b como 0.
>>> divide(2,5)
0.4
>>> divide(2,0)
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
Ahora hagamos que un decorador verifique este caso que causará el error.
def smart_divide(func):
def inner(a, b):
print("I am going to divide", a, "and", b)
if b == 0:
print("Whoops! cannot divide")
return
return func(a, b)
return inner
@smart_divide
def divide(a, b):
print(a/b)
Esta nueva implementación volverá None
si surge la condición de error.
>>> divide(2,5)
I am going to divide 2 and 5
0.4
>>> divide(2,0)
I am going to divide 2 and 0
Whoops! cannot divide
De esta manera podemos decorar funciones que toman parámetros.
Un observador entusiasta notará que los parámetros del anidado inner()
La función dentro del decorador es la misma que los parámetros de las funciones que decora. Teniendo esto en cuenta, ahora podemos hacer decoradores generales que funcionen con cualquier cantidad de parámetros.
En Python, esta magia se hace como function(*args, **kwargs)
. De este modo, args
será la tupla de argumentos posicionales y kwargs
será el diccionario de argumentos de palabras clave. Un ejemplo de tal decorador será:
def works_for_all(func):
def inner(*args, **kwargs):
print("I can decorate any function")
return func(*args, **kwargs)
return inner
Encadenamiento de decoradores en Python
Se pueden encadenar varios decoradores en Python.
Es decir, una función se puede decorar varias veces con decoradores diferentes (o iguales). Simplemente colocamos los decoradores encima de la función deseada.
def star(func):
def inner(*args, **kwargs):
print("*" * 30)
func(*args, **kwargs)
print("*" * 30)
return inner
def percent(func):
def inner(*args, **kwargs):
print("%" * 30)
func(*args, **kwargs)
print("%" * 30)
return inner
@star
@percent
def printer(msg):
print(msg)
printer("Hello")
Producción
****************************** %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Hello %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ******************************
La sintaxis anterior de,
@star
@percent
def printer(msg):
print(msg)
es equivalente a
def printer(msg):
print(msg)
printer = star(percent(printer))
El orden en el que encadenamos a los decoradores es importante. Si hubiéramos invertido el orden como,
@percent
@star
def printer(msg):
print(msg)
La salida sería:
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ****************************** Hello ****************************** %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%