2012-06-06 18 views
6

Así que recientemente hice una pregunta sobre la memorización y obtuve algunas respuestas excelentes, y ahora quiero llevarlo al siguiente nivel. Después de un poco de búsqueda en Google, no pude encontrar una implementación de referencia de un decorador de memoise que fue capaz de almacenar en caché una función que tomaba argumentos de palabra clave. De hecho, la mayoría de ellos simplemente usó *args como clave para búsquedas en caché, lo que significa que también se rompería si quisiera memorizar una función que aceptara listas o dictados como argumentos.¿Existe alguna forma pitónica de admitir argumentos de palabras clave para un decorador de memoizes en Python?

En mi caso, el primer argumento de la función es un identificador único por sí mismo, adecuado para usar como clave dict para búsquedas en caché, sin embargo, quería la capacidad de usar argumentos de palabras clave y acceder al mismo caché. Lo que quiero decir es que my_func('unique_id', 10) y my_func(foo=10, func_id='unique_id') deberían devolver el mismo resultado en caché.

Para hacer esto, lo que necesitamos es una manera limpia y pitónica de decir 'inspeccionar kwargs para cualquier palabra clave que corresponda al primer argumento)'. Esto es lo que se me ocurrió:

class memoize(object): 
    def __init__(self, cls): 
     if type(cls) is FunctionType: 
      # Let's just pretend that the function you gave us is a class. 
      cls.instances = {} 
      cls.__init__ = cls 
     self.cls = cls 
     self.__dict__.update(cls.__dict__) 

    def __call__(self, *args, **kwargs): 
     """Return a cached instance of the appropriate class if it exists.""" 
     # This is some dark magic we're using here, but it's how we discover 
     # that the first argument to Photograph.__init__ is 'filename', but the 
     # first argument to Camera.__init__ is 'camera_id' in a general way. 
     delta = 2 if type(self.cls) is FunctionType else 1 
     first_keyword_arg = [k 
      for k, v in inspect.getcallargs(
       self.cls.__init__, 
       'self', 
       'first argument', 
       *['subsequent args'] * (len(args) + len(kwargs) - delta)).items() 
        if v == 'first argument'][0] 
     key = kwargs.get(first_keyword_arg) or args[0] 
     print key 
     if key not in self.cls.instances: 
      self.cls.instances[key] = self.cls(*args, **kwargs) 
     return self.cls.instances[key] 

Lo loco es que esto realmente funciona. Por ejemplo, si se decoran de esta manera:

@memoize 
class FooBar: 
    instances = {} 

    def __init__(self, unique_id, irrelevant=None): 
     print id(self) 

A continuación, a partir del código se puede llamar a cualquiera FooBar('12345', 20) o FooBar(irrelevant=20, unique_id='12345') y consigue realmente la misma instancia de FooBar. Luego puede definir una clase diferente con un nombre diferente para el primer argumento, porque funciona de una manera general (es decir, el decorador no necesita saber nada específico sobre la clase que está decorando para que esto funcione).

El problema es que se trata de un desastre impío ;-)

Funciona porque inspect.getcallargs devuelve un diccionario mapear las palabras clave definidas a los argumentos que la suministra, por lo que los dan algunos argumentos falsos y luego inspeccione el dict de el primer argumento que se aprobó.

Lo que sería mucho más agradable, si tal cosa existiera, es un análogo al inspect.getcallargs que devuelve ambos tipos de argumentos unificados como una lista de los argumentos en lugar de como un dict de los argumentos de la palabra clave. Eso permitiría que algo como esto:

def __call__(self, *args, **kwargs): 
    key = inspect.getcallargsaslist(self.cls.__init__, None, *args, **kwargs)[1] 
    if key not in self.cls.instances: 
     self.cls.instances[key] = self.cls(*args, **kwargs) 
    return self.cls.instances[key] 

La otra manera de que pueda ver de abordar este estaría utilizando el dict proporcionada por inspect.getcallargs como la clave de caché de búsqueda directamente, pero eso requeriría una manera repetible para hacer cadenas idénticas de hashes idénticos, que es algo que he oído que no se puede confiar (supongo que tendré que construir la cadena después de ordenar las teclas).

¿Alguien tiene alguna idea al respecto? ¿Es incorrecto querer llamar a una función con argumentos de palabra clave y almacenar en caché los resultados? ¿O simplemente muy difícil?

Respuesta

4

me gustaría sugerir algo como lo siguiente:

import inspect 

class key_memoized(object): 
    def __init__(self, func): 
     self.func = func 
     self.cache = {} 

    def __call__(self, *args, **kwargs): 
     key = self.key(args, kwargs) 
     if key not in self.cache: 
      self.cache[key] = self.func(*args, **kwargs) 
     return self.cache[key] 

    def normalize_args(self, args, kwargs): 
     spec = inspect.getargs(self.func.__code__).args 
     return dict(kwargs.items() + zip(spec, args)) 

    def key(self, args, kwargs): 
     a = self.normalize_args(args, kwargs) 
     return tuple(sorted(a.items())) 

Ejemplo:

@key_memoized 
def foo(bar, baz, spam): 
    print 'calling foo: bar=%r baz=%r spam=%r' % (bar, baz, spam) 
    return bar + baz + spam 

print foo(1, 2, 3) 
print foo(1, 2, spam=3)   #memoized 
print foo(spam=3, baz=2, bar=1) #memoized 

Nota que también puede ampliar key_memoized y anular su método key() para proporcionar estrategias de memorización más específicas , p.ej. hacer caso omiso de algunos de los argumentos:

class memoize_by_bar(key_memoized): 
    def key(self, args, kwargs): 
     return self.normalize_args(args, kwargs)['bar'] 

@memoize_by_bar 
def foo(bar, baz, spam): 
    print 'calling foo: bar=%r baz=%r spam=%r' % (bar, baz, spam) 
    return bar 

print foo('x', 'ignore1', 'ignore2') 
print foo('x', 'ignore3', 'ignore4') 
+0

Me gusta el aspecto de esto, pero me preocupa la parte donde 'key' devuelve' tuple (a.items()) '. ¿Está garantizado que las claves se clasifican en el mismo orden para los dicts distintos pero idénticos? He oído que los dict están desordenados y se basan en cosas como 'str ({1: 2,3: 4})' para producir cadenas repetibles con una entrada idéntica. Muy, muy mal. – robru

+0

Parece que 'inspect.getargspec (func) .args [0]' es la respuesta precisa a la pregunta específica que hice (cómo encontrar el nombre del primer argumento), pero me gustaría expandir esto en una una solución más general. Voy a publicar mis resultados más tarde una vez que tenga algo de tiempo para perfeccionarlo. – robru

+0

@Robru: buen punto sobre la clasificación de dictos. Cambiado a 'tuple (ordenado (a.items()))' (otra opción sería 'frozenset (a.items())'). – georg

3

Trate lru_cache:

@functools.lru_cache(maxsize=128, typed=False)

decorador para envolver una función con un exigible memoizing que ahorra hasta el maxsize llamadas más recientes. Puede ahorrar tiempo cuando se llama periódicamente una función costosa o vinculada a E/S con los mismos argumentos.

lru_cache añadió en Python 3.2, pero puede ser portado en 2.x

+0

interesante leer sobre, pero desafortunadamente no funciona en mi situación porque tengo staticmethods clase que deben ser capaces de iterar sobre los casos en caché, por lo que el propio caché necesita estar expuesto como un atributo de clase. – robru

Cuestiones relacionadas