Un decorador toma una función, agrega alguna funcionalidad y la devuelve. En este tutorial, aprenderá cómo puede crear un decorador y por qué debería usarlo.
Decoradores en Python
Python tiene una característica interesante llamada decoradores para agregar funcionalidad a un código existente.
Esto también se denomina 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 estar 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 excepciones, 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")
Salida
Hola hola
Cuando ejecuta el código, ambas funciones first
y second
dan el mismo resultado. Aquí, los nombres first
y se second
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 lo sabe.
Las funciones que toman otras funciones como argumentos también se denominan 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()
Salida
Hola
Aquí, is_returned()
hay una función anidada que se define y devuelve cada vez que llamamos is_called()
.
Finalmente, debemos conocer los cierres en Python.
Volviendo a los decoradores
Las funciones y los métodos se denominan invocables como se pueden llamar.
De hecho, cualquier objeto que implemente el __call__()
método especial 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 un 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 decoradora agregó alguna funcionalidad nueva a la función original. Esto es similar a empacar un regalo. El decorador actúa como envoltorio. La naturaleza del objeto que fue decorado (regalo real en el interior) no se altera. Pero ahora, se ve bonito (desde que lo decoraron).
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 utilizar el @
símbolo junto con el nombre de la función decoradora y colocarlo 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.
Decoración de funciones con parámetros
El decorador anterior era simple y solo funcionaba con funciones que no tenían ningún parámetro. ¿Y si tuviéramos funciones que tomaran parámetros como:
def divide(a, b): return a/b
Esta función tiene dos parámetros, ay b. Sabemos que dará un 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 un decorador para verificar 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 regresará 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 atento notará que los parámetros de la inner()
función anidada dentro del decorador son los mismos que los parámetros de las funciones que decora. Teniendo esto en cuenta, ahora podemos hacer decoradores generales que funcionen con cualquier número de parámetros.
In Python, this magic is done as function(*args, **kwargs)
. In this way, args
will be the tuple of positional arguments and kwargs
will be the dictionary of keyword arguments. An example of such a decorator will be:
def works_for_all(func): def inner(*args, **kwargs): print("I can decorate any function") return func(*args, **kwargs) return inner
Chaining Decorators in Python
Multiple decorators can be chained in Python.
This is to say, a function can be decorated multiple times with different (or same) decorators. We simply place the decorators above the desired function.
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")
Output
****************************** %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Hello %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ******************************
The above syntax of,
@star @percent def printer(msg): print(msg)
is equivalent to
def printer(msg): print(msg) printer = star(percent(printer))
The order in which we chain decorators matter. If we had reversed the order as,
@percent @star def printer(msg): print(msg)
The output would be:
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ****************************** Hello ****************************** %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%