Advertisement
  1. Code
  2. Python

No repitas tu código de Python usando decoradores

Scroll to top
Read Time: 9 min

Spanish (Español) translation by Carlos (you can also view the original English article)

Los decoradores son una de las funciones más atractivas de Python, sin embargo, para el programador principiante, pueden parecer como magia. El propósito de este artículo es entender, en profundidad, el mecanismo detrás de los decoradores de Python.

Esto es lo que aprenderás:

  • qué son los decoradores de Python y para qué sirven
  • cómo definir nuestros propios decoradores
  • ejemplos de decoradores reales y cómo funcionan
  • cómo escribir mejor código usando decoradores

Introducción

En caso de que no hayas visto uno aún (o quizá no sabías que estabas tratando con uno de ellos), los decoradores se ven así:

1
@decorator
2
def function_to_decorate():
3
    pass

Generalmente los encuentras sobre la definición de una función, y tienen el prefijo @. Los decoradores son especialmente buenos para mantener tu código bajo el principio Dry (Don't Repeat Yourself o No te repitas, en español), y lo hacen al mismo tiempo que mejoran la legibilidad de tu código.

¿Aún no está claro? No te preocupes, pues los decoradores son solamente funciones de Python. ¡Así es! Ya sabes cómo crear uno. De hecho, el principio fundamental de los decoradores es la composición de funciones. Veamos un ejemplo:

1
def x_plus_2(x):
2
    return x + 2
3
4
print(x_plus_2(2))                      # 2 + 2 == 4

5
6
7
def x_squared(x):
8
    return x * x
9
10
print(x_squared(3))                     # 3 ^ 2 == 9

11
12
13
# Let's compose the two functions for x=2

14
print(x_squared(x_plus_2(2)))           # (2 + 2) ^ 2 == 16

15
print(x_squared(x_plus_2(3)))           # (3 + 2) ^ 2 == 25

16
print(x_squared(x_plus_2(4)))           # (4 + 2) ^ 2 == 36

¿Qué pasa si quisiéramos crear otra función, x_plus_2_squared? Intentar componer las funciones sería inútil:

1
x_squared(x_plus_2)  # TypeError: unsupported operand type(s) for *: 'function' and 'function'

No puedes componer funciones de esta forma porque ambas funciones toman los números como argumentos. No obstante, esto funcionará:

1
# Let's now create a proper function composition without actually applying the function

2
x_plus_2_squared = lambda x: x_squared(x_plus_2(x))
3
4
print(x_plus_2_squared(2)) # (2 + 2) ^ 2 == 16

5
print(x_plus_2_squared(3)) # (3 + 2) ^ 2 == 25

6
print(x_plus_2_squared(4)) # (4 + 2) ^ 2 == 36

Redefinamos cómo funciona x_squared. Si queremos que x_squared sea componible de forma predeterminada, debería:

  1. Aceptar una función como argumento
  2. Devolver otra función

Nombraremos la versión componible de x_squared simplemente como squared.

1
def squared(func):
2
    return lambda x: func(x) * func(x)
3
4
print(squared(x_plus_2)(2)) # (2 + 2) ^ 2 == 16

5
print(squared(x_plus_2)(3)) # (3 + 2) ^ 2 == 25

6
print(squared(x_plus_2)(4)) # (4 + 2) ^ 2 == 36

Ahora que hemos definido la función squared de forma que sea componible, podemos usarla con cualquier otra función. Aquí hay algunos ejemplos:

1
def x_plus_3(x):
2
    return x + 3
3
4
def x_times_2(x):
5
    return x * 2
6
7
print(squared(x_plus_3)(2))  # (2 + 3) ^ 2 == 25

8
print(squared(x_times_2)(2)) # (2 * 2) ^ 2 == 16

Podemos decir que squared decora las funciones x_plus_2, x_plus_3 y x_times_2. Estamos muy cerca de lograr la notación decoradora estándar. Mira esto:

1
x_plus_2 = squared(x_plus_2)  # We decorated x_plus_2 with squared

2
print(x_plus_2(2))            # x_plus_2 now returns the decorated squared result: (2 + 2) ^ 2 

¡Eso es! x_plus_2 es una función decorada de Python adecuada. Aquí es donde la notación @ entra en juego:

1
def x_plus_2(x):
2
    return x + 2
3
4
x_plus_2 = squared(x_plus_2)
5
6
# ^ This is completely equivalent with: 

7
8
@squared
9
def x_plus_2(x):
10
     return x + 2

De hecho, la notación @ es una forma de «syntactic sugar» (término que quiere decir que una sintaxis está diseñada para hacer las cosas más fáciles de leer, escribir o entender). Probemos eso:

1
@squared
2
def x_times_3(x):
3
    return 3 * x
4
5
print(x_times_3(2)) # (3 * 2) ^ 2 = 36.

6
# It might be a bit confusing, but by decorating it with squared, x_times_3 became in fact (3 * x) * (3 * x)

7
8
@squared
9
def x_minus_1(x):
10
    return x - 1
11
12
print(x_minus_1(3)) # (3 - 1) ^ 2 = 4

Si squared es el primer decorador que has escrito, date una gran palmada en la espalda. Has entendido uno de los conceptos más complejos de Python. Durante este proceso, aprendiste otra función fundamental de los lenguajes de programación funcional: la composición de funciones.

Crea tu propio decorador

Un decorador es una función que toma una función como argumento y devuelve otra función. Dicho lo anterior, la plantilla genérica para definir un decorador es:

1
def decorator(function_to_decorate):
2
    # ...

3
    return decorated_function

En caso de que no lo supieras, puedes definir funciones dentro de las funciones. En la mayoría de los casos, decorated_function se definirá dentro de decorator.

1
def decorator(function_to_decorate):
2
    def decorated_function(*args, **kwargs):
3
        # ... Since we decorate `function_to_decorate`, we should use it somewhere inside here

4
    return decorated_function

Veamos un ejemplo más práctico:

1
import pytz
2
from datetime import datetime
3
4
def to_utc(function_to_decorate):
5
    def decorated_function():
6
        # Get the result of function_to_decorate and transform the result to UTC

7
        return function_to_decorate().astimezone(pytz.utc)
8
    return decorated_function
9
10
@to_utc
11
def package_pickup_time():
12
    """ This can come from a database or from an API """
13
    tz = pytz.timezone('US/Pacific')
14
    return tz.localize(datetime(2017, 8, 2, 12, 30, 0, 0))
15
16
@to_utc
17
def package_delivery_time():
18
    """ This can come from a database or from an API """
19
    tz = pytz.timezone('US/Eastern')
20
    return tz.localize(datetime(2017, 8, 2, 12, 30, 0, 0)) # What a coincidence, same time different timezone!

21
22
print("PICKUP: ", package_pickup_time())      # '2017-08-02 19:30:00+00:00'

23
print("DELIVERY: ", package_delivery_time())  # '2017-08-02 16:30:00+00:00'

¡Excelente! Ahora puedes estar seguro de que todo dentro de tu aplicación está estandarizado para la zona horaria UTC.

Un ejemplo práctico

Otro ejemplo de uso muy popular y clásico para los decoradores es el almacenamiento en caché del resultado de una función:

1
import time
2
3
def cached(function_to_decorate):
4
    _cache = {} # Where we keep the results

5
    def decorated_function(*args):
6
        start_time = time.time()
7
        print('_cache:', _cache)
8
        if args not in _cache:
9
            _cache[args] = function_to_decorate(*args) # Perform the computation and store it in cache

10
        print('Compute time: %ss' % round(time.time() - start_time, 2))
11
        return _cache[args]
12
    return decorated_function
13
14
@cached
15
def complex_computation(x, y):
16
    print('Processing ...')
17
    time.sleep(2)
18
    return x + y
19
20
print(complex_computation(1, 2)) # 3, Performing the expensive operation

21
print(complex_computation(1, 2)) # 3, SKIP performing the expensive operation

22
print(complex_computation(4, 5)) # 9, Performing the expensive operation

23
print(complex_computation(4, 5)) # 9, SKIP performing the expensive operation

24
print(complex_computation(1, 2)) # 3, SKIP performing the expensive operation

Si miras el código de manera superficial, podrías objetar. ¡El decorador no es reutilizable! Si decoramos otra función (digamos another_complex_computation) y la llamamos con los mismos parámetros, entonces obtendremos los resultados en caché de complex_computation function. Esto no ocurrirá. El decorador es reutilizable, y aquí está el porqué:

1
@cached
2
def another_complex_computation(x, y):
3
    print('Processing ...')
4
    time.sleep(2)
5
    return x * y
6
    
7
print(another_complex_computation(1, 2)) # 2, Performing the expensive operation

8
print(another_complex_computation(1, 2)) # 2, SKIP performing the expensive operation

9
print(another_complex_computation(1, 2)) # 2, SKIP performing the expensive operation

La función cached se llama una vez por cada función que decora, por lo que una variable _cache diferente crea una instancia cada vez y se guarda en ese contexto. Probemos esto:

1
print(complex_computation(10, 20))           # -> 30

2
print(another_complex_computation(10, 20))   # -> 200

Decoradores en el mundo real

Como habrás notado, el decorador que acabamos de codificar es muy útil. Es tan útil que ya existe una versión más compleja y robusta en el módulo estándar functools. Se llama lru_cache. LRU es la abreviatura de Least Recently Used (menos utilizado recientemente), una estrategia de almacenamiento en caché.

1
from functools import lru_cache
2
3
@lru_cache()
4
def complex_computation(x, y):
5
    print('Processing ...')
6
    time.sleep(2)
7
    return x + y
8
9
print(complex_computation(1, 2)) # Processing ... 3

10
print(complex_computation(1, 2)) # 3

11
print(complex_computation(2, 3)) # Processing ... 5

12
print(complex_computation(1, 2)) # 3

13
print(complex_computation(2, 3)) # 5

Uno de mis usos favoritos de los decoradores es en el web framework Flask. Es tan limpio que este fragmento de código es lo primero que ves en el sitio web de Flask. Aquí está dicho fragmento:

1
from flask import Flask
2
3
app = Flask(__name__)
4
5
@app.route("/")
6
def hello():
7
    return "Hello World!"
8
9
if __name__ == "__main__":
10
    app.run()

El decorador app.route asigna la función hello como controlador de solicitudes para la ruta "/". La simplicidad es asombrosa.

Otro uso limpio de los decoradores es dentro de Django. Generalmente, las aplicaciones web tienen dos tipos de páginas:

  1. páginas que puedes ver sin autentificarse (página principal, página de aterrizaje, entradas de blog, inicio de sesión, registro)
  2. páginas que necesitas autentificar para ver (configuración de perfil, bandeja de entrada, panel de control)

Si tratas de ver una página de este último tipo, por lo general serás redirigido a una página de inicio de sesión. Aquí está cómo implementar eso en Django:

1
from django.http import HttpResponse
2
from django.contrib.auth.decorators import login_required
3
4
# Public Pages

5
6
def home(request):
7
    return HttpResponse("<b>Home</b>")
8
9
def landing(request):
10
    return HttpResponse("<b>Landing</b>")
11
12
# Authenticated Pages

13
14
@login_required(login_url='/login')
15
def dashboard(request):
16
    return HttpResponse("<b>Dashboard</b>")
17
18
@login_required(login_url='/login')
19
def profile_settings(request):
20
    return HttpResponse("<b>Profile Settings</b>")

Observa cómo las vistas privadas están marcadas con login_required. Mientras se revisa el código, está muy claro para el lector qué páginas requieren que el usuario inicie sesión y cuáles no.

Conclusiones

Espero que te hayas divertido aprendiendo sobre decoradores, ya que representan una función muy limpia de Python. Aquí hay algunos puntos a recordar:

  • El uso y el diseño correcto de los decoradores puede hacer que tu código sea mejor, más limpio y más bonito.
  • El uso de los decoradores puede ayudarte a mantener tu código bajo el principio DRY, mover el código idéntico de las funciones internas a los decoradores.
  • A medida que utilices más los decoradores, encontrarás mejores y más complejas maneras de usarlos.

Recuerda revisar lo que tenemos disponible para la venta y para estudiar en Envato Market, y no dudes en hacer cualquier pregunta y ofrecer tus valiosos comentarios utilizando el feed de abajo.

Pues bien, eso es todo sobre los decoradores. ¡Feliz «decoración»!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.