2012-08-06 9 views
6

La siguiente función está destinada a ser utilizada como un decorador que almacena los resultados de los valores ya computados. Si el argumento ya ha sido calculada antes, la función devolverá el valor almacenado en el diccionario cache:¿Cómo pueden los datos ser persistentes en múltiples llamadas de función decorada?

def cached(f): 
    f.cache = {} 
    def _cachedf(*args): 
     if args not in f.cache: 
      f.cache[args] = f(*args) 

     return f.cache[args] 

    return _cachedf 

me di cuenta (por error) que cache no tiene que ser un atributo del objeto función. Como cuestión de hecho, el código siguiente funciona así:

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 

Estoy teniendo un tiempo difícil entender cómo puede el objeto cache ser persistentes a través de múltiples llamadas. Intenté llamar varias funciones en caché varias veces y no pude encontrar ningún conflicto o problema.

¿Alguien puede ayudarme a comprender cómo existe la variable cache incluso después de que se devuelve la función _cachedf?

Respuesta

11

Usted está creando un closure aquí: La función _cachedf() cierra sobre la variable cache del alcance adjunto. Esto mantiene cache vivo mientras el objeto de función viva.

Editar: Tal vez debería agregar algunos detalles más sobre cómo funciona esto en Python y cómo CPython implementa esto. mirada

Vamos a un ejemplo más simple:

def f(): 
    a = [] 
    def g(): 
     a.append(1) 
     return len(a) 
    return g 

Ejemplo de uso en el intérprete interactivo

>>> h = f() 
>>> h() 
1 
>>> h() 
2 
>>> h() 
3 

Durante la compilación del módulo que contiene la función f(), la compilador ve que la función g() referencias el nombre a del que abarca el alcance y memoriza esta referencia externa en el código objeto cor responde a la función f() (específicamente, agrega el nombre a al f.__code__.co_cellvars).

Entonces, ¿qué ocurre cuando se llama a la función f()? La primera línea crea un nuevo objeto de lista y lo vincula con el nombre a. La siguiente línea crea un nuevo objeto de función (utilizando un objeto de código creado durante la compilación del módulo) y lo vincula con el nombre g. El cuerpo de g() no se ejecuta en este punto, y finalmente se devuelve el objeto funciton .

Dado que el objeto de código de f() tiene una nota que el nombre es a referenciado por las funciones locales, se crea una "célula" de este nombre cuando se introduce f(). Esta celda contiene la referencia a la lista real objeto a y la función g() hace referencia a esta celda. De esta forma, el objeto de la lista y la celda se mantienen activos incluso cuando sale el funciton f().

+0

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

+0

@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. –

3

¿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.

+0

¿Cómo funciona esto para python decorator? Cada vez que llamo 'cached (foo) (2)', no almacena en caché el resultado y duerme dos segundos. ¿Cada llamada de función decorada hace referencia al mismo decorador? –

+0

@SiminJie - Ver mi edición adicional a la respuesta. – brice

Cuestiones relacionadas