Decoradores en Python (III) - Clases decoradoras
Este post fue migrado de un blog hecho con Wordpress. Si se ve mal, dejame un comentario y lo arreglo.
Siguiendo con la serie de posts sobre decoradores en Python, y fiel al espíritu que los originó (ir mostrando lo que voy aprendido a medida que necesito resolver problemas específicos o descubro aplicaciones concretas) hoy les traigo un nuevo uso para los decoradores en Python: funciones caché.
Anteriormente: Decoradores I, Decoradores II.
Funciones caché
Una función caché[0], es aquella que siempre que se le pide que compute un resultado para un grupo de parámetros dado, primero se fija en una memoria interna si no realizó ya el cálculo. Si ya lo hizo, retorna el valor computado anteriormente. Si aún no lo hizo, computa el valor, lo guarda en una memoria interna y luego lo retorna.
Esta técnica es muy útil en funciones que requieren un cómputo intensivo y obtener un resultado lleva mucho tiempo. Permita acelerar sustancialmente los tiempos de ejecución a cambio de utilizar más memoria.
La siguiente es una forma de implementarlo en Python para un computo en particular:
cache = {} def fmem(arg): if arg in cache: print "Recuperando valor de la memoria" return cache[arg] else: r = (arg ** 10) * (arg ** -5) cache[arg] = r return r
Como memoria se utiliza un diccionario y el argumento de la función fmem es la clave del diccionario[1].
Este es el resultado de utilizarla en el intérprete interactivo:
>>> fmem(1) 1.0 >>> fmem(2) 32.0 >>> fmem(2) Recuperando valor de la memoria 32.0
Decoradores con estado
En esta implementación, la técnica de memorización se mezcla con el cálculo que era el objetivo original de la función. Si queremos aplicar la técnica sobre distintas funciones vamos a tener que entrometer la implementación de la caché en todas las funciones. Peor aún, si en el futuro se quiere realizar un cambio en la forma de almacenar y recuperar los valores almacenados, ¡tendríamos que modificar todas las funciones! La forma de resolver estos problemas es implementando un decorador que agregue esta funcionalidad a las funciones decoradas: resolvemos ambos problemas, el de intrución y el de mantenibilidad. Todo el código que provee esta funcionalidad extra es encapsulado en el decorador.
Las funciones decoradoras, como las que vimos en los anteriores artículos, no nos sirven para esta tarea. Necesitamos un decorador que pueda almacenar un estado. Ya que cualquier callable puede ser un decorador, implementaremos el decorador mediante una clase.
Funciones caché con clases decoradoras
La definición de la clase decoradora consiste en dos métodos:
- un método de inicialización, dónde se inicializa el atributo cache con un diccionario vacío y se guarda una referencia a la función decorada.
- un método __call__ que será ejecutado cuando se llame a la función decorada.
class mem(object): def __init__(self, g): self.cache = {} self.g = g def __call__(self, arg): if arg in self.cache: print "Recuperando valor de la memoria" return self.cache[arg] else: r = self.g(arg) self.cache[arg] = r return r
Luego, lo único que resta es decorar todas las funciones que querramos "dotar de memoria" para obtener mejoras de performance en su ejecución:
@mem def f(arg): return (arg ** 10) * (arg ** -5)
La salida obtenida al ejecutar la función decorada en el intérprete interactivo es la misma qué en el ejemplo anterior:
>>> fmem(1) 1.0 >>> fmem(2) 32.0 >>> fmem(2) Recuperando valor de la memoria 32.0
Más
La implementación del decorador mem solo sirve para decorar funciones que reciben un único argumento. Podemos mejorar su definición para que pueda decorar funciones con cualquier número de argumentos:
class mem2(object): def __init__(self, g): self.cache = {} self.g = g def __call__(self, *args): if args in self.cache: print "Recuperando valor de la memoria" return self.cache[args] else: r = self.g(*args) self.cache[args] = r return r
@mem2 def f2(arg1, arg2): return (arg1 ** 10) * (arg2 ** -5)
Notas
[0] Se puede leer más sobre este concepto en Caching Function Results:Faster Arithmetic by Avoiding Unnecessary Computation de Stephen E. Richardson [SMLI TR-92-1]
[1] Esta implementación tiene la limitación de que si el argumento de la función es un objeto mutable, no podrá ser usado como clave de un diccionario y se lanzará una excepción.
Comentarios
Comments powered by Disqus