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 un int 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 o int.
  • 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"
Screenshot of a program that uses python decorators

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
Screenshot-from-2023-05-23-02-33-18

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
Screenshot-from-2023-05-23-02-20-38

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!
Screenshot-from-2023-05-23-02-02-07

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!
Screenshot-from-2023-05-23-02-02-07-1

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!
Screenshot-from-2023-05-23-02-07-18

@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
Screenshot-from-2023-05-23-02-09-42

@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
Screenshot-from-2023-05-23-11-46-25

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.