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?
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
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
@Robru: buen punto sobre la clasificación de dictos. Cambiado a 'tuple (ordenado (a.items()))' (otra opción sería 'frozenset (a.items())'). – georg