Decoradores en Python
Un decorador es una función permite cambiar el comportamiento de un método sin modificarlo directamente. En Python todo se puede considerar un objeto por eso esto es posible:
def first(a):
def second(b):
return a + b
return second
>>> f = first(5)
>>> s = f(7)
12
>>> first(3)(5)
8
Un decorador no es lo mismo que el patrón de diseño decorador como se advierte en el PEP 318 de Python Software Foudation:
On the name ‘Decorator’ There’s been a number of complaints about the choice of the name ‘decorator’ for this feature. The major one is that the name is not consistent with its use in the GoF book. The name ‘decorator’ probably owes more to its use in the compiler area – a syntax tree is walked and annotated. It’s quite possible that a better name may turn up.
El siguiente decorador calcula el tiempo de ejecucución entre dos llamados a la misma función:
import time
def cronometro(func):
'calcula el tiempo entre ejecuciones de la funcion decorada'
def wrapper(*args, **kwargs):
if wrapper.time:
tiempo_transcurrido = time.time() - wrapper.time
print(f"Tiempo transcurrido: {tiempo_transcurrido}")
wrapper.time = time.time()
return func(*args, **kwargs)
wrapper.time = 0
return wrapper
def suma_v1(a, b):
return a + b
# Podemos decorar la función asi:
>>> suma_v1 = cronometro(suma_v1)
>>> suma_v1(2, 3)
5
# Otra forma más común:
@cronometro
def suma_v2(a, b):
return a + b
>>> suma(5, 3)
8
>>> suma(4,3)
Tiempo transcurrido: 5.103554010391235
7
Uso de wraps y lru_cache de functools
Los atributos como __name__
, __doc__
y __module__
de las funciones originales se pierden de modo que al ejecutar:
print(foo.__name__)
aparece el nombre de la función wrapper. Para evitar esto usamos wraps:
from functools import wraps
def deco(func):
@wraps(func)
def wrapper(*args):
print(func.__name__)
return func(*args)
return wrapper
@deco
def hola():
print("saludos")
>>> hola()
hola
saludos
>>> hola.__name__
'hola'
# Sin wraps aparecería:
>>> hola.__name__
wrapper
LRU significa last recently used y se usa para guardar en la memoria cache los resultados intermedios en las llamadas de las funciones recursivas. Por ejemplo, para calcular la secuencia de fibonacci si se se usa el caché, la velocidad de ejecución mejora drmáticamente:
import time
from functools import lru_cache
def timer(f):
'decorador para calcular tiempo de ejecución'
def wrapper(*args):
t1 = time.time()
res = f(*args)
print(f"duracion: {time.time() - t1}")
return res
return wrapper
@lru_cache(maxsize=None)
def fibo1(n):
if n < 2:
return n
return fibo(n-1) + fibo(n-2)
def fibo2(n):
if n < 2:
return n
return fibo2(n-1) + fibo2(n-2)
@timer
test_fibo1():
fibo1(36)
@timer
test_fibo2():
fibo2(36)
>>> test_fibo1()
duracion: 4.220008850097656e-05
>>> test_fibo2() # Más de 100 mil veces más lento
duracion: 7.274502754211426
Clases como decoradores
Para usar una clase como decorador se debe utilizar la función mágica __call__
que sirve para que un objeto se
comporte como función:
class decorator:
def __init__(self, func):
self.func = func
def __call__(self):
print(self.func.__name__)
self.func()
@decorator
def foo()
print("hello")