42

Decoradores en Python: qué son y cómo funcionan

52047Puntos

hace 2 años

Un decorador en Python es una función que recibe otra función como parámetro, le añade cosas y retorna una función diferente. Son herramientas muy útiles. Nos permiten envolver una función dentro de otra y modificar el comportamiento de esta última sin modificarla permanentemente.

Esto suena algo confuso. Y es que esto es un tema avanzado dentro de Python. No obstante, en este artículo vas a aprender qué son, como funcionan y también programarás tus primeros operadores en Python. Este blog se divide en 3 partes:

  • Funciones. Una recapitulación para tener lo necesario y entender como funcionan los decoradores.
  • Decoradores en Python. Comprenderás todo sobre decoradores en Python.
  • Reto. Con todo lo aprendido, te dejo un reto. ¿Serás capaz de resolverlo?

He preparado este repositorio en GitHub para que puedas complementar tu aprendizaje de este blog.

https://images.unsplash.com/photo-1612443016610-00c5fa0ec439?ixlib=rb-4.0.3&q=80&fm=jpg&crop=entropy&cs=tinysrgb

¿Qué son las funciones en Python?

Antes de entrar de lleno en el tema de decoradores en Python, tenemos que tener muy claro cómo es la lógica de las funciones en python. Entonces, por definición, una función es un pedazo de código que toma valores de entrada, los procesa y devuelve otros valores. En Python, la su sintaxis es como está a continuación.

defhello(name):return'Hello, {name}!'.format(name=name)	

La función anterior recibe un argumento name que lo procesa y devuelve un saludo. También podemos asignar la función a una variable.

hola = hello
print(hola)
print(hola('Axel'))

Fíjate que asignamos hello a hola. Pero no usamos el paréntesis () dado que solo queremos asignar la referencia a la función, pero no ejecutarla. Por lo tanto, print(hola) nos devuelve la clase y su referencia de espacio en memoria, mientras que print(hola('Axel')) nos devuelve la ejecución de la función que es el saludo. En la terminal, se vería lo siguiente.

<function hello at 0x7fc826f0df30>
Hello, Axel!

¿Cómo se definen funciones dentro de otras funciones en Python?

También es posible definir funciones dentro de otras funciones. Presta mucha atención a esto porque es fundamental para entender como funcionan los decoradores.

defparent():
    print('This is parent function.')
    
    definner_1():
        print('This is inner function 1.')
    
    definner_2():
        print('This is inner function 2.')
    
    inner_1()
    inner_2()

parent()

El código anterior imprime en consola lo siguiente:

This is parent function.
This is inner function 1.
This is inner function 2.

¿Qué pasó aquí? El primer print statement ejecuta el This is parent function. de la función padre. Luego se definen dos funciones que son llamadas y se ejecutan. Pero también podemos hacer un return de la referencia de la función. En algunos casos es más práctico.
Veamos otro ejemplo:

defchoose_num(num):defopt_1():return'You choose 9.'defopt_2():return'You didn\'t choose 9.'if num == 9:
        return opt_1

    else:
        return opt_2

option_1 = choose_num(9)
option_2 = choose_num(2)

print(option_1) #prints the class
print(option_1()) #prints the returned string

En este caso, como se hace un return de la función en sí, el primer print imprime la clase y su espacio en memoria, mientras que el segundo imprime el string que devuelve la función. Esto se ve en la terminal.

<function choose_num.<locals>.opt_1 at 0x7f187e0a9ea0>
You choose 9.

Funciones que reciben otra función como parámetro

Veamos en código como pasar una función como argumento de otra.

defduplicate(num):return num * 2deftest_function(func):
    num_2_duplicate = 13return func(num_2_duplicate)

print(test_function(duplicate))

Aquí el resultado que imprime la terminal es 26. Mira cómo definimos duplicate y luego se la pasamos como argumento a test_function para que duplique 13.

¿Cómo crear decoradores en Python?

Ha sido una introducción larguísima. Sorry. Pero créeme que vale la pena haber dejado claro como funcionan las funciones. Y como ya tienes toda esta base, es hora de crear decoradores. Pero antes recuerda la definición que pusimos al inicio de este artículo:

Un decorador en Python es una función que recibe otra función como parámetro, le añade cosas y retorna una función diferente.

Lo siguiente es un decorador.

defmake_upper(func):defwrapper():return func().upper()
    
    return wrapper

defpython_greeting():return'hi, i\'m a Python developer!'

python_greeting = make_upper(python_greeting)  #This is what decorates the function

print(python_greeting())

La función que recibe otra función es make_upper que recibe el argumento func que es una función. Dentro, se modifica a func usando wrapper. Y finalmente devuelve wrapper que es func pero diferente.

La función que se decora es python_greeting y la línea de código que lo hace es:

python_greeting = make_upper(python_greeting)

Mira que implícitamente nunca modificamos python_greeeting. Solo usamos un decorador para convertir su output en mayúsculas. La terminal nos arroja:

HI, I'M A PYTHON DEVELOPER!

¿Qué pasaría si imprimimos solo la referencia a la función con print(python_greeting)? Exacto, solo se mostraría la clase a la que pertenece y su espacio en memoria.

<function make_upper.<locals>.wrapper at 0x7f896e8c20e0>

Y así es como se crea un decorador en Python.

Pero espera.

Hay una forma más elegante para escribir código pythónico. Agreguémosle azúcar a nuestra sintaxis.

Decoradores pythónicos: syntactic sugar

La manera anterior que usamos para crear decoradores en Python es válida. Funciona al 100%. Pero tú quieres hacerlo de forma más escalable, elegante y profesional. Te muestro abajo nuestro decorador anterior, pero con su sintaxis mejorada.

defmake_upper(func):defwrapper():return func().upper()
    
    return wrapper

@make_upperdefpython_greeting():return'hi, i\'m a Python developer!'

print(python_greeting())

Mira lo chévere que queda nuestro código ahora. Incluso te ahorras un par de líneas y lo haces más legible para que otra persona pueda entenderlo.

Nota que usamos el operador @ para decorar la función y obtener el mismo output. Esta es la forma profesional de hacerlo y la que te recomiendo.

¿Se puede decorar varias veces una función en Python?

Claro que podemos decorar varias veces una sola función. No obstante, ten en cuenta que esto ocurre en el orden en que llamas a los decoradores. Veamos un ejemplo en el que, además del decorador anterior, aplicamos uno que invierta el orden de las letras que le pasamos.

defmake_upper(func):defwrapper():return func().upper()
    
    return wrapper

defreverse_str(func):defwrapper():return func()[::-1] 
    
    return wrapper   

@make_upper@reverse_strdefpython_greeting():return'hi, i\'m a Python developer!'

print(python_greeting())

Internamente, primero se aplica @make_upper para convertir todo el string en mayúsculas y luego @reverse_str para invertir el orden de la misma. Por lo tanto, en la terminal veremos.

!REPOLEVED NOHTYP A M'I ,IH

¿Cómo usar argumentos en funciones decoradas y decoradores?

Es posible pasarles argumentos a la función decorada. Procedemos así.

defdecorate_sum(func):defwrapper(arg1, arg2):
        print(f'Values to sum are: {arg1} and {arg2}.')
        return func(arg1, arg2)
    return wrapper

@decorate_sumdefsum_nums(num_1, num_2):return num_1 + num_2

print(sum_nums(7, 9))

Como output obtenemos:

Values to sum are: 7 and 9.
16

Para extender las funcionalidades de los decoradores en Python al máximo, podemos pasarles argumentos. Mira un ejemplo de aquello.

defmultiply_result(num_3):definner_decorator(func):defwrapper(arg1, arg2):
            print(f'({arg1} + {arg2}) * {num_3}:')
            return func(arg1, arg2) * num_3 
        return wrapper
    return inner_decorator

@multiply_result(3)defsum_nums(num_1, num_2):return num_1 + num_2

print(sum_nums(1, 2))

Ahora, nuestro decorador multiply_result modifica la función sum_nums para que su resultado sea multiplicado por un número que el usuario elija ingresar. Nota que, en realidad, ingresamos 3 valores para poder obtener un resultado a nuestro gusto. La terminal nos muestra lo siguiente.

(1 + 2) * 3:
9

💡Pro tip: como cada vez se vuelve más complejo el tema de decoradores, te recomiendo:

  • Primero, escribe la función que quieres decorar. En el caso anterior sum_nums.
  • Luego, piensa en cómo quieres modificar esta función.
  • Entonces, programa la función decoradora, multiply_result.
  • Finalmente, agrega la línea que decora la función, @multiply_result.

Te aseguro que con este approach se te hará más sencillo crear tus decoradores.

Decoradores con múltiples variables: *args y **kwargs

Los argumentos especiales *args y **kwargs nos permiten pasar múltiples positional arguments y keyword arguments. Esto es una excelente ventaja para escribir código pythónico y escalable. Te dejo una documentación sobre esto por si deseas indagar más.

Veamos a nuestro decorador anterior ahora modificado para que pueda recibir tantos argumentos como nosotros queramos.

defmultiply_result(*args_parent):definner_decorator(func):defwrapper(*args):
            nums_sum = args
            nums_multiply = sum(args_parent)
            print(f'sum{nums_sum} * {nums_multiply}:')
            return func(*args) * sum(args_parent)
        return wrapper
    return inner_decorator

@multiply_result(3, 4, 5)defsum_nums(*args):return sum(args)

print(sum_nums(1, 2))

Modificamos el decorador y su función de tal forma que podamos pasar tantos argumentos como queramos. El código anterior hace que sum_nums reciba varios números y los suma. multiply_result hace lo mismo, pero este valor es multiplicado con aquel retornado de sum_nums.

🍓La frutilla en el postre: cómo debuggear decoradores en Python

¿Recuerdas nuestro primer decorador pythónico? Sí, ese que lo codeamos de una forma profesional y escalable. Pues ahora, te voy a enseñar un truco que hará que destaques como software developer y lleves tu código al siguiente nivel. Teníamos lo siguiente.

defmake_upper(func):defwrapper():"""Wrapper function"""return func().upper()
    
    return wrapper

@make_upperdefpython_greeting():"""Python greeting function"""return'hi, i\'m a Python developer!'

Nota que aquí le agregamos los docstrings"""Wrapper function""" y """Python greeting function""" para documentar nuestras funciones. Esto es una buena práctica. Pues bien, ¿que pasaría si quisiéramos acceder a los atributos de python_greeting, por ejemplo, __name__ o __doc__? ¿Qué imprimiría el siguiente código?

print(python_greeting.__name__)
print(python_greeting.__doc__)

Detente un momento a pensarlo.

Tómate tu tiempo.

¿Ya lo pensaste?

Bueno, la terminal nos devuelve lo siguiente.

wrapper
Wrapper function

Pero, en realidad, hubiéramos querido obtener python_greeting y Python greeting function. ¿Cómo podemos hacer esto? Usando el decorador de functools.wraps. Para esto importamos esta librería que viene en Python.

import functools

defmake_upper(func):    @functools.wraps(func)  #This decorator changes the debugging rules UwUdefwrapper():"""Wrapper function"""return func().upper()
    
    return wrapper

@make_upperdefpython_greeting():"""Python greeting function"""return'hi, i\'m a Python developer!'

Mira como llegamos a un concepto más avanzado y pusimos un decorador dentro del decorador con el fin de poder acceder a los verdaderos atributos de nuestra función decorada python_greeting. Y con esto puedes hacer un debugging más efectivo para tu código.

print(python_greeting.__name__)
print(python_greeting.__doc__)

El output del anterior código es:

python_greeting
Python greeting function

Y con esto has cambiado las reglas del juego a tu favor para debuggear. Como toda una gran desarrolladora o desarrollador de software.

Reto: construye tu propio decorador en Python

Con este artículo llevaste tus habilidades de desarrollo al siguiente nivel. Ahora podrás construir aplicaciones de una forma más pythónica y profesional. Con esto marcarás la diferencia.

Ahora, es el momento de poner a prueba lo aprendido con el siguiente reto:

Con todo lo que aprendiste sobre decoradores en Python, crea un decorador que incluya todos estos puntos, desde pasar argumentos a la función decorada y al decorador, hasta usar @functools.wraps para acceder a la información debuggeable de la función decorada.

Con este reto tienes la oportunidad de dejar volar tu imaginación y crear cosas increíbles. Recuerda dejar tu código en la sección de comentarios para que tus compañeros y compañeras te puedan dar feedback e inspirarse. También puedes llevar tu solución a Discord. En Platzi crecemos en comunidad.

Ten presente el repo en GitHub que te deje al inicio de este blog. Puede servirte de guía, ya que allí encuentras todo el código sobre decoradores en Python que usamos aquí. Te dejo la solución de este reto acá.

Tu código puede ser totalmente diferente al mío. En mi caso, decoré la sucesión de Fibonacci para usar un recurso que se llama memoization que sirve para mejorar el performance.

Finalmente, me gustaría invitarte a tomar el Curso de Python: PIP y Entornos Virtuales para que complementes tus conocimientos sobre decoradores.

¡Nunca pares de aprender Python! Sigue nuestra ruta de data science con Python🐍

Axel
Axel
axl-yaguana

52047Puntos

hace 2 años

Todas sus entradas
Escribe tu comentario
+ 2
Ordenar por:
3
66523Puntos

¡El blog ha estado increíble! Me encantaron los ejemplos con kwargs y args. ¡Guardaré este blog como una herramienta que nos ayudará más tarde!

1
52047Puntos
2 años

¡Qué bien, Francisco! Me alegra que todo este contenido te fue útil.

3
9004Puntos

Con esto reforce el curso de python

1
52047Puntos
2 años

¡Eso es, Victor!

Nunca pares de aprender.

2
13Puntos

hola como estas, me parece muy interesante el tema de progrmación, pero no sé nada de ese tema, me podrias decir por donde empezar y qué debo estudiar?

1
52047Puntos
2 años

¡Hola, Jorge! ¿Ya viste el Nuevo Curso Gratis de Programación Básica? Te recomiendo comenzar por aquí.

Si bien es JavaScript, este curso te dará las bases para que puedas pivotar a cualquier otro lenguaje que escojas.

Cuando empece a programar me hubiera gustado que existiera un curso así. Por esto te lo recomiendo al 100%.

2
23274Puntos

Muy muy bueno. Gracias 🧡

¿Puedo pedir uno igualito pero de funciones asíncronas? 🥺 por fis?

2
22616Puntos

Buenísimo el aporte!! Super claro! Muy buen trabajo!