Apéndice T — Decoradores.

Objetivo. Explicar como se pueden agregar características a una función de python sin modificarla.

T.1 Definición.

  • Se denomina decorador a la persona dedicada a diseñar el interior de oficinas, viviendas o establecimientos comerciales con criterios estéticos y funcionales.
  • En Python, un decorador es una función para modificar otra función.
    • Recibe una función.
    • Regresa otra función.
  • Los decoradores son herramientas bonitas y útiles de Python.
Ejemplo T.1:

La función print_hello() imprime Hola mundo pythonico.

def print_hello():
    print('{:^30}'.format('Hola mundo pythonico'))
  • Crear un decorador que agregue colores al mensaje.
def print_hello():
    print('{:^30}'.format('Hola mundo pythonico'))
# Uso normal de la función
print_hello()
     Hola mundo pythonico     
from colorama import Fore, Back, Style

# Decorador
def decorador1(f):

    # La función que hace el decorado.
    def envoltura():
        linea = chr(0x2015) * 30
        print(Fore.BLUE + linea + Style.RESET_ALL)
        print(Back.GREEN + Fore.WHITE, end='')
        
        f() # Ejecución de la función que se recibe como parámetro
        
        print(Style.RESET_ALL, end='')
        print(Fore.BLUE + linea + Style.RESET_ALL)

    return envoltura # Regresamos la función decorada
# Decorando la función.
print_hello_colored = decorador1(print_hello) # Funcion decorada

# Ahora se ejecuta la función decorada.
print_hello_colored()
――――――――――――――――――――――――――――――

     Hola mundo pythonico     

――――――――――――――――――――――――――――――

.

Ejemplo T.2:

La función print_message(m) imprime el mensaje que recibe como parámetro.

def print_message(m):
    print('{:^30}'.format(m))
  • Modificar el decorador creado en el ejemplo T.1 para que se pueda recibir el parámetro m.
# Decorador
def decorador2(f):

    # La función que hace el decorado.
    # Ahora recibe un parámetro
    def envoltura(m):
        linea = chr(0x2015) * 30
        print(Fore.BLUE + linea + Style.RESET_ALL)
        print(Back.GREEN + Fore.WHITE, end='')
        
        f(m) # Ejecución de la función que se recibe como parámetro
        
        print(Style.RESET_ALL, end='')
        print(Fore.BLUE + linea + Style.RESET_ALL)

    return envoltura # Regresamos la función decorada
# La función se puede decorar en su definición como sigue
@decorador2
def print_message(m):
    print('{:^30}'.format(m))
# Entonces se puede usar la función con su nombre original
print_message('bueno, bonito y barato')
――――――――――――――――――――――――――――――

    bueno, bonito y barato    

――――――――――――――――――――――――――――――
print_message('el bueno, el malo y el feo')
――――――――――――――――――――――――――――――

  el bueno, el malo y el feo  

――――――――――――――――――――――――――――――

.

Ejemplo T.3:

Decorar las funciones sin() y cos() de la biblioteca math.

def decorador3(f):

    def coloreado(x):

        # Construimos una cadena coloreada con el 
        # resultado de la evaluación de f(x)
        res = Fore.GREEN + f.__name__ 
        res += '(' + Style.BRIGHT + str(x) + Style.RESET_ALL + Fore.GREEN + ') = ' + Style.RESET_ALL
        
        res += f'{f(x):10.8f}' # Aquí se ejectua la función original

        # Imprimimos el resultado
        linea = chr(0x2015) * 30
        print(Fore.BLUE + linea + Style.RESET_ALL)
        print('{:}'.format(res))
        print(Fore.BLUE + linea + Style.RESET_ALL)

    return coloreado
from math import sin, cos

sin = decorador3(sin)
cos = decorador3(cos)

for f in [sin, cos]:
    f(3.1415)
――――――――――――――――――――――――――――――

sin(3.1415) = 0.00009265

――――――――――――――――――――――――――――――

――――――――――――――――――――――――――――――

cos(3.1415) = -1.00000000

――――――――――――――――――――――――――――――

El decorador se puede usar con otras funciones similares:

from math import factorial, log, e

factorial = decorador3(factorial)
log = decorador3(log)

factorial(5)
log(e)
――――――――――――――――――――――――――――――

factorial(5) = 120.00000000

――――――――――――――――――――――――――――――

――――――――――――――――――――――――――――――

log(2.718281828459045) = 1.00000000

――――――――――――――――――――――――――――――

.

Ejemplo T.4:

Decorar funciones con un número variable de argumentos.

def decorador4(f):
    
    def envoltura(*args, **kwargs):

        # Construimos una cadena coloreada con el 
        # resultado de la evaluación de f(x)
        res = Fore.GREEN + f.__name__ 
        res += '(' + Style.BRIGHT + f'{args},{kwargs}' + Style.RESET_ALL + Fore.GREEN + ') = ' + Style.RESET_ALL
        res += f'{f(*args, **kwargs)}'
        
        # Imprimimos el resultado
        linea = chr(0x2015) * 30
        print(Fore.BLUE + linea + Style.RESET_ALL)
        print('{:}'.format(res))
        print(Fore.BLUE + linea + Style.RESET_ALL)
        
    return envoltura
from random import random, randint, choice, choices

random = decorador4(random)
randint = decorador4(randint)
choice = decorador4(choice)
choices = decorador4(choices)
p = [x for x in range(10)]

random()
randint(3, 30)
choice(p)
choices(p)
choices(p,k=3)
――――――――――――――――――――――――――――――

random((),{}) = 0.13026213383709218

――――――――――――――――――――――――――――――

――――――――――――――――――――――――――――――

randint((3, 30),{}) = 29

――――――――――――――――――――――――――――――

――――――――――――――――――――――――――――――

choice(([0, 1, 2, 3, 4, 5, 6, 7, 8, 9],),{}) = 7

――――――――――――――――――――――――――――――

――――――――――――――――――――――――――――――

choices(([0, 1, 2, 3, 4, 5, 6, 7, 8, 9],),{}) = [5]

――――――――――――――――――――――――――――――

――――――――――――――――――――――――――――――

choices(([0, 1, 2, 3, 4, 5, 6, 7, 8, 9],),{'k': 3}) = [2, 8, 1]

――――――――――――――――――――――――――――――

.

Ejemplo T.5:

Crear un decorador que calcule el tiempo de ejecución de una función.

import time

def crono(f):
    """
    Regresa el tiempo que toma en ejecutarse la función.
    """
    def tiempo(N):
        t1 = time.perf_counter()
        f(N)
        t2 = time.perf_counter()
        return print(f'Elapsed time: {t2 - t1}\n')

    return tiempo
@crono
def miFuncion(N):
    numeros = []
    for num in (range(0, N)):
        numeros.append(num)
    print('\nLa suma es: ' + str((sum(numeros))))
miFuncion(10)

La suma es: 45
Elapsed time: 0.0001438999897800386

.

Ejemplo T.6:

Detener la ejecución por un tiempo antes que una función sea ejecutada.

from time import sleep

def sleepDecorador(function):

    def duerme(*args, **kwargs):
        sleep(1)
        return function(*args, **kwargs)
        
    return duerme
@sleepDecorador
def imprimeNumero(num):
    return num

for num in range(1, 6):
    print(imprimeNumero(num), end = ' ')
1 2 3 4 5 

.

Ejemplo T.7:

Crear un decorador que cheque que el argumento de una función sea un entero positivo y posteriormente que ejecute una función para calcular el factorial de dicho número.

def checaArgumento(f):
    def checador(x):
        if type(x) == int and x > 0:
            return f(x)
        else:
            raise Exception("El argumento no es un entero positivo")
    return checador
@checaArgumento
def factorial(n):
    if n == 1:
        return 1
    else:
        return n * factorial(n-1)
for i in range(1,10):
    print(i, factorial(i))
1 1
2 2
3 6
4 24
5 120
6 720
7 5040
8 40320
9 362880
print(factorial(-1))
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
Cell In[24], line 1
----> 1 print(factorial(-1))

Cell In[21], line 6, in checaArgumento.<locals>.checador(x)
      4     return f(x)
      5 else:
----> 6     raise Exception("El argumento no es un entero positivo")

Exception: El argumento no es un entero positivo

.

Ejemplo T.8:

Contar el número de llamadas de una función.

def contadorDeLlamadas(func):
    
    def cuenta(*args, **kwargs):
        cuenta.llamadas += 1
        return func(*args, **kwargs)
        
    # Variable estática que lleva la cuenta
    cuenta.llamadas = 0
    
    return cuenta
@contadorDeLlamadas
def suma(x):
    return x + 1

@contadorDeLlamadas
def mulp1(x, y=1):
    return x*y + 1
print('Llamadas a suma = {}'.format(suma.llamadas))
print('Llamadas a multp1 = {}'.format(mulp1.llamadas))
Llamadas a suma = 0
Llamadas a multp1 = 0
for i in range(4):
    suma(i)
    
mulp1(1, 2)
mulp1(5)
mulp1(y=2, x=25)

print('Llamadas a suma = {}'.format(suma.llamadas))
print('Llamadas a multp1 = {}'.format(mulp1.llamadas))
Llamadas a suma = 4
Llamadas a multp1 = 3

.

Ejemplo T.9:

Decorar una función con diferentes saludos.

def buenasTardes(func):
    def saludo(x):
        print("Hola, buenas tardes, ", end='')
        func(x)
    return saludo

def buenosDias(func):
    def saludo(x):
        print("Hola, buenos días, ", end='')
        func(x)
    return saludo
@buenasTardes
def mensaje1(hora):
    print("son las " + hora)

mensaje1("3 pm")

@buenosDias
def mensaje2(hora):
    print("son las " + hora)
    
mensaje2("8 am")
Hola, buenas tardes, son las 3 pm
Hola, buenos días, son las 8 am

.

Ejemplo T.10:

El ejemplo anterior se puede realizar como sigue:

def saludo(expr):
    
    def saludoDecorador(func):
        def saludoGenerico(x):
            print(expr, end='')
            func(x)
        return saludoGenerico
        
    return saludoDecorador
@saludo("Hola, buenas tardes, ")
def mensaje1(hora):
    print("son las " + hora)

mensaje1("3 pm")

@saludo("Hola, buenos días, ")
def mensaje2(hora):
    print("son las " + hora)
    
mensaje2("8 am")

@saludo("καλημερα ")
def mensaje3(hora):
    print(" <--- en griego " + hora)
    
mensaje3(" :D ")
Hola, buenas tardes, son las 3 pm
Hola, buenos días, son las 8 am
καλημερα  <--- en griego  :D