2012-02-22 10 views
8

Obviamente, una búsqueda rápida produce un millón de implementaciones y sabores del decorador de memorias en Python. Sin embargo, estoy interesado en un sabor que no he podido encontrar. Me gustaría tenerlo tal que la memoria caché de valores almacenados pueda tener una capacidad fija. Cuando se agregan nuevos elementos, si se alcanza la capacidad, se elimina el valor más antiguo y se reemplaza con el valor más nuevo.¿Cómo creo un decorador de memorando delimitado en Python?

Mi preocupación es que, si uso la memorización para almacenar muchos elementos, el programa se bloqueará debido a la falta de memoria. (No sé cuán bien ubicada esta preocupación puede ser en la práctica.) Si la memoria caché era de un tamaño fijo, entonces un error de memoria no sería un problema. Y muchos problemas en los que trabajo cambian a medida que el programa se ejecuta, de modo que los valores iniciales en caché se verían muy diferentes de los valores almacenados en caché posteriores (y sería mucho menos probable que se repitan más adelante). Es por eso que me gustaría que las cosas más antiguas sean reemplazadas por las más nuevas.

Encontré la clase OrderedDict y un ejemplo que muestra cómo subclase para especificar un tamaño máximo. Me gustaría usar eso como mi caché, en lugar de un dict normal. El problema es que necesito que el decorador de memoize tome un parámetro llamado maxlen cuyo valor predeterminado es None. Si es None, entonces el caché no tiene límites y funciona normalmente. Cualquier otro valor se usa como el tamaño de la memoria caché.

quiero que funcione como la siguiente:

@memoize 
def some_function(spam, eggs): 
    # This would use the boundless cache. 
    pass 

y

@memoize(200) # or @memoize(maxlen=200) 
def some_function(spam, eggs): 
    # This would use the bounded cache of size 200. 
    pass 

A continuación se muestra el código que tengo hasta ahora, pero no veo cómo pasar el parámetro en el decorador mientras lo hace funcionar tanto "desnudo" como con un parámetro.

import collections 
import functools 

class BoundedOrderedDict(collections.OrderedDict): 
    def __init__(self, *args, **kwds): 
     self.maxlen = kwds.pop("maxlen", None) 
     collections.OrderedDict.__init__(self, *args, **kwds) 
     self._checklen() 

    def __setitem__(self, key, value): 
     collections.OrderedDict.__setitem__(self, key, value) 
     self._checklen() 

    def _checklen(self): 
     if self.maxlen is not None: 
      while len(self) > self.maxlen: 
       self.popitem(last=False) 

def memoize(function): 
    cache = BoundedOrderedDict() # I want this to take maxlen as an argument 
    @functools.wraps(function) 
    def memo_target(*args): 
     lookup_value = args 
     if lookup_value not in cache: 
      cache[lookup_value] = function(*args) 
     return cache[lookup_value] 
    return memo_target 

@memoize 
def fib(n): 
    if n < 2: return 1 
    return fib(n-1) + fib(n-2) 

if __name__ == '__main__': 
    x = fib(50) 
    print(x) 

Editar: El uso de la sugerencia de Ben, que crea el siguiente decorador, que creo que funciona de la manera que imaginaba. Es importante para mí poder usar estas funciones decoradas con multiprocessing, y eso ha sido un problema en el pasado. Pero una prueba rápida de este código parecía funcionar correctamente, incluso cuando se trabaja en un conjunto de hilos.

def memoize(func=None, maxlen=None): 
    if func: 
     cache = BoundedOrderedDict(maxlen=maxlen) 
     @functools.wraps(func) 
     def memo_target(*args): 
      lookup_value = args 
      if lookup_value not in cache: 
       cache[lookup_value] = func(*args) 
      return cache[lookup_value] 
     return memo_target 
    else: 
     def memoize_factory(func): 
      return memoize(func, maxlen=maxlen) 
     return memoize_factory 

Respuesta

4
@memoize 
def some_function(spam, eggs): 
    # This would use the boundless cache. 
    pass 

Aquí memoize se utiliza como una función que se llama en un único argumento de la función, y devuelve una función. memoize es un decorador.

@memoize(200) # or @memoize(maxlen=200) 
def some_function(spam, eggs): 
    # This would use the bounded cache of size 200. 
    pass 

Aquí memoize se utiliza como una función que se llama en un solo argumento entero y devuelve una función, y que la función devuelve es en sí utilizado como un decorador es decir, que se llama en un solo argumento de la función y devuelve una función . memoize es una fábrica decoradora .

Para unificar estos dos, tendrá que escribir un código feo. La forma en que probablemente hacerlo es tener memoize tener este aspecto:

def memoize(func=None, maxlen=None): 
    if func: 
     # act as decorator 
    else: 
     # act as decorator factory 

De esta manera si desea pasar parámetros que siempre pasar argumentos de palabras clave, dejando func (que debería ser un parámetro de posición) desarmado, y si solo quiere que todo funcione de manera predeterminada, mágicamente funcionará como un decorador directamente. Esto significa que @memoize(200) le dará un error; se puede evitar haciendo una comprobación de tipo para ver si func es invocable, lo que debería funcionar bien en la práctica pero no es realmente muy "pitónico".

Una alternativa sería tener dos decoradores diferentes, por ejemplo memoize y bounded_memoize. El ilimitado memoize puede tener una implementación trivial simplemente llamando al bounded_memoize con maxlen establecido en None, por lo que no le cuesta nada en la implementación o el mantenimiento.

Normalmente, como regla general, trato de evitar que se modifique una función para implementar dos conjuntos de funcionalidades relacionados tangencialmente, especialmente cuando tienen firmas diferentes. Pero en este caso hace que el use del decorador es natural (que requiere @memoize() sería bastante propenso a errores, aunque es más consistente desde una perspectiva teórica), y presumiblemente va a implementar esto una vez y usarlo muchos veces, por lo que la lectura en el punto de uso es probablemente la preocupación más importante.

-2

De http://www.python.org/dev/peps/pep-0318/

La sintaxis actual también permite a las declaraciones del decorador para llamar a una función que devuelve un decorador:

@decomaker(argA, argB, ...) 
def func(arg1, arg2, ...): 
    pass 

Esto es equivalente a:

func = decomaker(argA, argB, ...)(func) 

Además, No estoy seguro si usaría OrderedDict para esto, usaría un anillo Buffe r, son muy fáciles de implementar.

0

¿Quieres escribir un decorador que toma un argumento (la longitud máxima de la BoundedOrderedDict) y devuelve un decorador que memoize su función con un BoundedOrderedDict del tamaño apropiado:

def boundedMemoize(maxCacheLen): 
    def memoize(function): 
     cache = BoundedOrderedDict(maxlen = maxCacheLen) 
     def memo_target(*args): 
      lookup_value = args 
      if lookup_value not in cache: 
       cache[lookup_value] = function(*args) 
      return cache[lookup_value] 
     return memo_target 
    return memoize 

Usted puede usarlo como esto:

@boundedMemoize(100) 
def fib(n): 
    if n < 2: return 1 
    return fib(n - 1) + fib(n - 2) 

Editar: Vaya, se perdió parte de la pregunta. Si desea que el argumento maxlen para el decorador sea opcional, puede hacer algo como esto:

def boundedMemoize(arg): 
    if callable(arg): 
     cache = BoundedOrderedDict() 
     @functools.wraps(arg) 
     def memo_target(*args): 
      lookup_value = args 
      if lookup_value not in cache: 
       cache[lookup_value] = arg(*args) 
      return cache[lookup_value] 
     return memo_target 

    if isinstance(arg, int): 
     def memoize(function): 
      cache = BoundedOrderedDict(maxlen = arg) 
      @functools.wraps(function) 
      def memo_target(*args): 
       lookup_value = args 
       if lookup_value not in cache: 
        cache[lookup_value] = function(*args) 
       return cache[lookup_value] 
      return memo_target 
     return memoize 
+0

No del todo. En la pregunta pedí algo que podría funcionar de manera equivalente con y sin el parámetro. No creo que este pueda hacer eso. – agarrett

+0

Lo siento, he editado mi respuesta. – srgerg

Cuestiones relacionadas