Un decorador es una función que recibe como parámetro otra función, le añade cosas y retorna una función diferente, básicamente estamos modificando a esta función principal sin tener que escribir directamente mas código.

def decorador(func):
	def envoltura():
		print(f'esto se le añdade a la funcion original')
		func()
	return envoltura

def saludo():
	print("hola")

saludo()

saludo=decorador(saludo)
saludo()

Crear una función y después decorarla, sigue un patron común, por lo mismo se puede definir de una manera mas "pythonica"

Azúcar sintáctica

es un código que esta embellecido para que sea mas fácil de entender

Para decorar una función basta con colocar, en su parte superior de dicha función, el decorador con el prefijo @.

def decorador(func):
	def envoltura():
		print(f'esto se le añdade a la funcion original')
		func()
	return envoltura

@decorador
def saludo()
	print("hola")

¿Qué pasa si nuestra función a decorar debe recibir argumentos y a su vez debe retornar algún valor? en estos casos haremos uso de los parámetros args y kwargs. En el siguiente ejemplo se va a medir el tiempo de ejecución de varias funciones por medio de un decorador

from datetime import datetime

def execution_time(fun):
    def wrapper(*args,**kwargs):
        start_time=datetime.now()
        fun(*args,**kwargs)
        end_time=datetime.now()
        total_time=end_time-start_time
        print(f'the elapsed time was: {total_time.total_seconds()} seconds')
    return wrapper

@execution_time
def series():
    for _ in range(1, 100000000):
        pass
@execution_time
def suma(a,b):
    return a+b

@execution_time
def saludo(string):
    print(f'hola{string}')

series()
suma(1,2)
saludo('pablito')