¿Alguien puede ayudarme a entender cómo la variable de caché todavía existe incluso después de que se devuelve la función _cachedf?
Tiene que ver con el colector de basura de conteo de Python. La variable cache
se conservará y será accesible ya que la función _cachedf
tiene una referencia y la persona que llama al cached
tiene una referencia. Cuando vuelve a llamar a la función, todavía está utilizando el mismo objeto de función que se creó originalmente, por lo tanto, todavía tiene acceso a la memoria caché.
No perderá la memoria caché hasta que se destruyan todas las referencias a la misma. Puede usar el operador del
para hacer eso.
Por ejemplo:
>>> import time
>>> def cached(f):
... cache = {} # <---- not an attribute this time!
... def _cachedf(*args):
... if args not in cache:
... cache[args] = f(*args)
... return cache[args]
... return _cachedf
...
...
>>> def foo(duration):
... time.sleep(duration)
... return True
...
...
>>> bob = cached(foo)
>>> bob(2) # Takes two seconds
True
>>> bob(2) # returns instantly
True
>>> del bob # Deletes reference to bob (aka _cachedf) which holds ref to cache
>>> bob = cached(foo)
>>> bob(2) # takes two seconds
True
>>>
Para el registro, lo que estás tratando de acheive se llama Memoization, y hay un decorador memoizing más completa disponible en el decorator pattern page, que hace lo mismo, pero usando una decorador clase. Su código y el decorador basado en la clase son esencialmente iguales, con el decorador basado en la clase que comprueba la habilidad hash antes de almacenar.
edición (02/02/2017): @SiminJie comenta que cached(foo)(2)
siempre incurre en un retraso.
Esto se debe a que cached(foo)
devuelve una nueva función con una memoria caché nueva. Cuando se llama al cached(foo)(2)
, se crea un nuevo caché nuevo (vacío) y luego se llama inmediatamente a la función en caché.
Como la memoria caché está vacía y no va a encontrar el valor, vuelve a ejecutar la función subyacente. En su lugar, haga cached_foo = cached(foo)
y luego llame al cached_foo(2)
varias veces. Esto solo incurrirá en el retraso para la primera llamada. Además, si se utiliza como un decorador, que funcionará como se esperaba:
@cached
def my_long_function(arg1, arg2):
return long_operation(arg1,arg2)
my_long_function(1,2) # incurs delay
my_long_function(1,2) # doesn't
Si no está familiarizado con decoradores, echar un vistazo a this answer de entender lo que significa el código de seguridad.
Muchas gracias por la explicación, su edición deja las cosas muy claras. Me pregunto, por el bien de aprender las partes internas de (C) Python si es posible acceder a la "celda" que mencionas en tu último párrafo, a través de una inspección o algo similar. – rahmu
@rahmu: Corregí un error en la explicación (que no cambia demasiado). Desafortunadamente, las celdas son completamente transparentes para el código Python y siempre son reemplazadas por el objeto al que se refieren, por lo que no pueden ser examinadas. –