Los decoradores en Python son una construcción increíblemente útil en Python. Usando decoradores en Python, podemos modificar el comportamiento de una función envolviéndola dentro de otra función. Los decoradores nos permiten escribir código más limpio y compartir funcionalidad. Este artículo es un tutorial no sólo sobre cómo utilizar decoradores, sino también sobre cómo crearlos.
Conocimientos previos
El tema de los decoradores en Python requiere algunos conocimientos previos. A continuación, he enumerado algunos conceptos con los que ya debería estar familiarizado para que este tutorial tenga sentido. También he enlazado recursos donde puede repasar los conceptos si es necesario.
Python básico
Este tema es un tema más intermedio/avanzado. Como resultado, antes de intentar aprenderlo, ya debería estar familiarizado con los conceptos básicos de Python, como los tipos de datos, las funciones, los objetos y las clases.
También debería comprender algunos conceptos orientados a objetos, como los getters, setters y constructores. Si no está familiarizado con el lenguaje de programación Python, aquí tiene algunos recursos para empezar.
Las funciones son ciudadanos de primera clase
Además de los conocimientos básicos de Python, también debería conocer este concepto más avanzado de Python. Las funciones, y prácticamente todo lo demás en Python, son objetos como int
o string
. Debido a que son objetos, puede hacer algunas cosas con ellos, a saber:
- Puede pasar una función como argumento a otra función del mismo modo que pasa una
cadena
o unint
como argumento de una función. - Las funciones también pueden ser devueltas por otras funciones del mismo modo que devolvería otros valores
string
oint
. - Las funciones pueden almacenarse en variables
De hecho, la única diferencia entre los objetos funcionales y otros objetos es que los objetos funcionales contienen el método mágico __call__()
.
Esperemos que, llegados a este punto, se sienta cómodo con los conocimientos previos. Podemos empezar a discutir el tema principal.
¿Qué es un decorador de Python?
Un decorador de Python es simplemente una función que toma una función como argumento y devuelve una versión modificada de la función que se le pasó. En otras palabras, la función foo es un decorador si toma como argumento la función bar
y devuelve otra función baz
.
La función baz
es una modificación de bar
en el sentido de que dentro del cuerpo de baz
hay una llamada a la función bar
. Sin embargo, antes y después de la llamada a bar
, baz
puede hacer cualquier cosa. Eso ha sido un trabalenguas; he aquí algo de código para ilustrar la situación:
# Foo es un decorador, toma otra función, bar como argumento
def foo(bar):
# Aquí creamos baz, una versión modificada de bar
# baz llamará a bar pero puede hacer cualquier cosa antes y después de la llamada a la función
def baz():
# Antes de llamar a bar, imprimimos algo
print("Algo")
# Luego ejecutamos bar haciendo una llamada a la función
bar()
# Luego imprimimos algo más después de ejecutar bar
print("Algo más")
# Por último, foo devuelve baz, una versión modificada de bar
return baz
¿Cómo crear un decorador en Python?
Para ilustrar cómo se crean y utilizan los decoradores en Python, voy a ilustrarlo con un ejemplo sencillo. En este ejemplo, crearemos una función decoradora logger que registrará el nombre de la función que está decorando cada vez que esa función se ejecute.
Para empezar, creamos la función decoradora. El decorador recibe func
como argumento. func
es la función que estamos decorando.
def crear_logger(func):
# El cuerpo de la función va aquí
Dentro de la función decoradora, vamos a crear nuestra función modificada que registrará el nombre de func
antes de ejecutar func
.
# Dentro de create_logger
def modified_func():
print("Llamando a: ", func.__name__)
func()
A continuación, la función create_logger
devolverá la función modificada. Como resultado, toda nuestra función create_logger
tendrá este aspecto:
def crear_logger(func):
def modified_func():
print("Llamando a: ", func.__name__)
func()
return función_modificada
Hemos terminado de crear el decorador. La función create_logger
es un ejemplo sencillo de función decoradora. Toma func
, que es la función que estamos decorando, y devuelve otra función, modified_func. modified_func
registra primero el nombre de func
, antes de ejecutar func
.
Cómo utilizar decoradores en Python
Para utilizar nuestro decorador, utilizamos la sintaxis @
de esta forma
@crear_logger
def decir_hola():
print("¡Hola, mundo!")
Ahora podemos llamar a say_hello() en nuestro script, y la salida debería ser el siguiente texto:
Llamada: say_hello
"Hola, mundo"
Pero, ¿qué está haciendo el @create_logger
? Pues está aplicando el decorador a nuestra función say_hello. Para entender mejor lo que está haciendo, el código inmediatamente inferior a este párrafo conseguiría el mismo resultado que poniendo @create_logger
antes de say_hello
.
def decir_hola():
print("¡Hola, mundo!")
say_hello = create_logger(say_hello)
En otras palabras, una forma de utilizar decoradores en Python es llamar explícitamente al decorador pasando la función como hicimos en el código anterior. La otra forma, más concisa, es utilizar la sintaxis @
.
En esta sección, hemos cubierto cómo crear decoradores en Python.
Ejemplos algo más complicados
El ejemplo anterior era un caso sencillo. Hay ejemplos un poco más complejos como cuando la función que estamos decorando toma argumentos. Otra situación más complicada es cuando se quiere decorar una clase entera. Voy a cubrir ambas situaciones aquí.
Cuando la función toma en argumentos
Cuando la función que está decorando toma argumentos, la función modificada debe recibir los argumentos y pasarlos cuando eventualmente haga la llamada a la función no modificada. Si esto le parece confuso, permítame explicárselo en términos de foo-bar.
Recuerde que foo
es la función decoradora, bar
es la función que estamos decorando y baz
es la barra
decorada. En ese caso, bar tomará los argumentos y se los pasará a baz
durante la llamada a baz
. He aquí un ejemplo de código para solidificar el concepto:
def foo(bar):
def baz(*args, **kwargs):
# Puede hacer algo aquí
___
# Luego hacemos la llamada a bar, pasando args y kwargs
bar(*args, **kwargs)
# También puede hacer algo aquí
___
return baz
Si los *args
y **kwargs
le resultan desconocidos; son simplemente punteros a los argumentos posicionales y de palabra clave, respectivamente.
Es importante tener en cuenta que baz
tiene acceso a los argumentos y, por tanto, puede realizar alguna validación de los mismos antes de llamar a bar
.
Un ejemplo sería si tuviéramos una función decoradora, ensure_string
que se asegurara de que el argumento pasado a una función que está decorando es una cadena; la implementaríamos así
def asegurar_cadena(func):
def función_decorada(texto):
if type(texto) is not str:
raise TypeError('el argumento a ' func.__name__ ' debe ser una cadena.')
si no
func(texto)
return func_decorada
Podríamos decorar la función decir_hola
así
@ensure_string
def decir_hola(nombre):
print('Hola', nombre)
Luego podríamos probar el código usando esto
say_hello('Juan') # Debería funcionar bien
say_hello(3) # Debería lanzar una excepción
Y debería producir la siguiente salida
Hola Juan
Traceback (most recent call last):
File "/home/anesu/Documents/python-tutorial/./decoradores.py", line 20, in <module> say hola(3) # debería lanzar una excepción
File "/home/anesu/Documents/python-tu$ ./decoradores.pytorial/./decoradores.py", line 7, in decorado_func raise TypeError('el argumento a func._nombre_ debe ser una cadena.')
TypeError: argumento para decir hola debe ser una cadena. $0
Como era de esperar, el script consiguió imprimir ‘Hola Juan’ porque ‘Juan’ es una cadena. Lanzó una excepción al intentar imprimir ‘Hola 3’ porque ‘3’ no era una cadena. El decorador ensure_string
podría utilizarse para validar los argumentos de cualquier función que requiera una cadena.
Decorando una clase
Además de sólo decorar funciones, también podemos decorar clases. Cuando añade un decorador a una clase, el método decorado sustituye al método constructor/iniciador de la clase (__init__).
Volviendo a foo-bar, supongamos que foo es nuestro decorador y Bar es la clase que estamos decorando, entonces foo decorará Bar.__init__. Esto será útil si queremos hacer algo antes de que los objetos de tipo Bar
sean instanciados.
Esto significa que el siguiente código
def foo(func):
def nueva_func(*args, **kwargs):
print('Haciendo algunas cosas antes de la instanciación')
func(*args, **kwargs)
return nueva_func
@foo
clase Bar:
def __init__(self):
print("En iniciador")
Es equivalente a
def foo(func):
def new_func(*args, **kwargs):
print('Haciendo algunas cosas antes de la instanciación')
func(*args, **kwargs)
return nueva_func
clase Bar:
def __init__(self):
print("En iniciador")
Bar.__init__ = foo(Bar.__init__)
De hecho, instanciar un objeto de la clase Bar, definido utilizando cualquiera de los dos métodos, debería darle la misma salida:
Haciendo algunas cosas antes de la instanciación
En iniciador
Ejemplos de decoradores en Python
Aunque puede definir sus propios decoradores, hay algunos que ya están incorporados en Python. Estos son algunos de los decoradores comunes que puede encontrar en Python:
@métodoestático
El método static se utiliza en una clase para indicar que el método que está decorando es un método estático. Los métodos estáticos son métodos que pueden ejecutarse sin necesidad de instanciar la clase. En el siguiente ejemplo de código, creamos una clase Perro
con un método estático ladrar
.
clase Perro
@métodoestático
def ladrar():
print('¡Guau, guau!')
Ahora se puede acceder al método ladrar
de la siguiente manera
Dog.bark()
Y la ejecución del código produciría la siguiente salida
¡Guau, guau!
Como mencioné en la sección Cómo utilizar decoradores, los decoradores pueden utilizarse de dos formas. Siendo la sintaxis @
la más concisa de las dos. El otro método es llamar a la función del decorador, pasando la función que queremos decorar como argumento. Es decir, el código de arriba consigue lo mismo que el código de abajo:
clase Perro:
def ladrar():
print('¡Guau, guau!')
Dog.bark = staticmethod(Dog.bark)
Y podemos seguir utilizando el método ladrar
de la misma manera
Dog.bark()
Y produciría la misma salida
¡Guau, guau!
Como puede ver, el primer método es más limpio y es más obvio que la función es una función estática incluso antes de haber empezado a leer el código. Como resultado, para los ejemplos restantes, utilizaré el primer método. Pero recuerde que el segundo método es una alternativa.
@classmethod
Este decorador se utiliza para indicar que el método que está decorando es un método de clase. Los métodos de clase son similares a los métodos estáticos en que ambos no requieren que la clase sea instanciada antes de que puedan ser llamados.
Sin embargo, la principal diferencia es que los métodos de clase tienen acceso a los atributos de la clase mientras que los métodos estáticos no. Esto se debe a que Python pasa automáticamente la clase como primer argumento a un método de clase siempre que es llamado. Para crear un método de clase en Python, podemos utilizar el decorador classmethod
.
clase Perro
@classmethod
def qué_eres(cls):
print("¡Soy un " cls.__name__ "!")
Para ejecutar el código, simplemente llamamos al método sin instanciar la clase:
Perro.que_eres_tu()
Y la salida es
¡Soy un Perro!
@propiedad
El decorador de propiedades se utiliza para etiquetar un método como definidor de propiedades. Volviendo a nuestro ejemplo del Perro, creemos un método que recupere el nombre del Perro.
clase Perro:
# Crear un método constructor que tome el nombre del perro
def __init__(self, nombre):
# Creando una propiedad privada nombre
# Los guiones bajos dobles hacen que el atributo sea privado
self.__name = nombre
@propiedad
def nombre(self):
return self.__name
Ahora podemos acceder al nombre del perro como una propiedad normal,
# Creando una instancia de la clase
foo = Perro('foo')
# Accediendo a la propiedad nombre
print("El nombre del perro es:", foo.name)
Y el resultado de ejecutar el código sería
El nombre del perro es: foo
@property.setter
El decorador property.setter se utiliza para crear un método setter para nuestras propiedades. Para utilizar el decorador @property
.setter, sustituya property
por el nombre de la propiedad para la que está creando un setter. Por ejemplo, si está creando un setter para el método de la propiedad foo, su decorador será @foo.setter
. He aquí un ejemplo de Perro para ilustrarlo:
clase Perro:
# Crear un método constructor que tome el nombre del perro
def __init__(self, nombre):
# Creando una propiedad privada nombre
# Los guiones bajos dobles hacen que el atributo sea privado
self.__name = nombre
@propiedad
def nombre(self):
return self.__name
# Creando un setter para nuestra propiedad nombre
@nombre.setter
def nombre(self, nuevo_nombre):
self.__nombre = nuevo_nombre
Para probar el setter, podemos utilizar el siguiente código:
# Creación de un nuevo perro
foo = Perro('foo')
# Cambiar el nombre del perro
foo.name = 'bar'
# Imprimiendo el nombre del perro en la pantalla
print("El nuevo nombre del perro es:", foo.name)
La ejecución del código producirá la siguiente salida
El nuevo nombre del perro es: bar
Importancia de los decoradores en Python
Ahora que hemos cubierto lo que son los decoradores, y usted ha visto algunos ejemplos de decoradores, podemos discutir por qué los decoradores son importantes en Python. Los decoradores son importantes por varias razones. Algunas de ellas, las enumero a continuación:
- Permiten la reutilización del código: En el ejemplo
de registro
dado anteriormente, podríamos utilizar el @create_logger en cualquier función que queramos. Esto nos permite añadir la funcionalidad de registro a todas nuestras funciones sin tener que escribirla manualmente para cada función. - Le permiten escribir código modular: De nuevo, volviendo al ejemplo del registro, con los decoradores, puede separar la función central, en este caso
say_hello
de la otra funcionalidad que necesita, en este caso, el registro. - Mejoran los marcos de trabajo y las bibliotecas: Los decoradores se utilizan ampliamente en los frameworks y librerías de Python para proporcionar funcionalidad adicional. Por ejemplo, en frameworks web como Flask o Django, los decoradores se utilizan para definir rutas, manejar la autenticación o aplicar middleware a vistas específicas.
Palabras finales
Los decoradores son increíblemente útiles; puede utilizarlos para extender funciones sin alterar su funcionalidad. Esto resulta útil cuando desea cronometrar el rendimiento de las funciones, registrar cada vez que se llama a una función, validar los argumentos antes de llamar a una función o verificar los permisos antes de que se ejecute una función. Una vez que entienda los decoradores, podrá escribir código de forma más limpia.
A continuación, puede que desee leer nuestros artículos sobre tuplas y el uso de cURL en Python.