python - library - Usando @ functools.lru_cache con los argumentos del diccionario
functools library python (6)
¿Qué hay de la subclase namedtuple
y agregar acceso por x["key"]
?
class X(namedtuple("Y", "a b c")):
def __getitem__(self, item):
if isinstance(item, int):
return super(X, self).__getitem__(item)
return getattr(self, item)
Tengo un método que toma (entre otros) un diccionario como argumento. El método consiste en analizar cadenas y el diccionario proporciona reemplazos para algunas subcadenas, por lo que no tiene que ser mutable.
Esta función se llama con bastante frecuencia, y en elementos redundantes, así que pensé que el almacenamiento en caché mejoraría su eficiencia.
Pero, como habrás adivinado, ya que dict
es mutable y, por lo tanto, no es hashable, @functools.lru_cache
no puede decorar mi función. Entonces, ¿cómo puedo superar esto?
Punto de bonificación si solo necesita clases y métodos de biblioteca estándar. Idealmente, si existe algún tipo de frozendict
en la biblioteca estándar que no he visto, sería mi día.
PD: se namedtuple
solo en último lugar, ya que necesitaría un gran cambio de sintaxis.
¿Qué pasa con la creación de una clase dict
hashable como tal:
class HDict(dict):
def __hash__(self):
return hash(frozenset(self.items()))
substs = HDict({''foo'': ''bar'', ''baz'': ''quz''})
cache = {substs: True}
Aquí hay un decorador que se puede utilizar como functools.lru_cache
. Pero esto está dirigido a funciones que toman solo un argumento que es un mapeo plano con valores de hashable y tiene un maxsize
fijo de 64. Para su caso de uso, tendría que adaptar este ejemplo o su código de cliente. Además, para establecer el maxsize
individualmente, uno tenía que implementar otro decorador, pero no he metido la cabeza en esto ya que no lo necesitaba.
from functools import (_CacheInfo, _lru_cache_wrapper, lru_cache,
partial, update_wrapper)
from typing import Any, Callable, Dict, Hashable
def lru_dict_arg_cache(func: Callable) -> Callable:
def unpacking_func(func: Callable, arg: frozenset) -> Any:
return func(dict(arg))
_unpacking_func = partial(unpacking_func, func)
_cached_unpacking_func = /
_lru_cache_wrapper(_unpacking_func, 64, False, _CacheInfo)
def packing_func(arg: Dict[Hashable, Hashable]) -> Any:
return _cached_unpacking_func(frozenset(arg.items()))
update_wrapper(packing_func, func)
packing_func.cache_info = _cached_unpacking_func.cache_info
return packing_func
@lru_dict_arg_cache
def uppercase_keys(arg: dict) -> dict:
""" Yelling keys. """
return {k.upper(): v for k, v in arg.items()}
assert uppercase_keys.__name__ == ''uppercase_keys''
assert uppercase_keys.__doc__ == '' Yelling keys. ''
assert uppercase_keys({''ham'': ''spam''}) == {''HAM'': ''spam''}
assert uppercase_keys({''ham'': ''spam''}) == {''HAM'': ''spam''}
cache_info = uppercase_keys.cache_info()
assert cache_info.hits == 1
assert cache_info.misses == 1
assert cache_info.maxsize == 64
assert cache_info.currsize == 1
assert uppercase_keys({''foo'': ''bar''}) == {''FOO'': ''bar''}
assert uppercase_keys({''foo'': ''baz''}) == {''FOO'': ''baz''}
cache_info = uppercase_keys.cache_info()
assert cache_info.hits == 1
assert cache_info.misses == 3
assert cache_info.currsize == 3
Para un enfoque más genérico, se podría usar el decorator @cachetools.cache de una biblioteca de terceros con una función adecuada configurada como key
.
Aquí hay un decorador que usa el truco de @mhyfritz.
def hash_dict(func):
"""Transform mutable dictionnary
Into immutable
Useful to be compatible with cache
"""
class HDict(dict):
def __hash__(self):
return hash(frozenset(self.items()))
@functools.wraps(func)
def wrapped(*args, **kwargs):
args = tuple([HDict(arg) if isinstance(arg, dict) else arg for arg in args])
kwargs = {k: HDict(v) if isinstance(v, dict) else v for k, v in kwargs.items()}
return func(*args, **kwargs)
return wrapped
Simplemente agréguelo antes de su lru_cache.
@hash_dict
@functools.lru_cache()
def your_function():
...
Después de decidir abandonar el caché lru para nuestro caso de uso por ahora, aún encontramos una solución. Este decorador utiliza json para serializar y deserializar los argumentos / kwargs enviados al caché. Funciona con cualquier número de argumentos. Úselo como un decorador en una función en lugar de @lru_cache. El tamaño máximo se establece en 1024.
def hashable_lru(func):
cache = lru_cache(maxsize=1024)
def deserialise(value):
try:
return json.loads(value)
except Exception:
return value
def func_with_serialized_params(*args, **kwargs):
_args = tuple([deserialise(arg) for arg in args])
_kwargs = {k: deserialise(v) for k, v in kwargs.items()}
return func(*_args, **_kwargs)
cached_function = cache(func_with_serialized_params)
@wraps(func)
def lru_decorator(*args, **kwargs):
_args = tuple([json.dumps(arg, sort_keys=True) if type(arg) in (list, dict) else arg for arg in args])
_kwargs = {k: json.dumps(v, sort_keys=True) if type(v) in (list, dict) else v for k, v in kwargs.items()}
return cached_function(*_args, **_kwargs)
lru_decorator.cache_info = cached_function.cache_info
lru_decorator.cache_clear = cached_function.cache_clear
return lru_decorator
En lugar de usar un diccionario de hashable personalizado, ¡use esto y evite reinventar la rueda! Es un diccionario congelado que es todo hashable.
https://pypi.org/project/frozendict/
Código:
def freezeargs(func):
"""Transform mutable dictionnary
Into immutable
Useful to be compatible with cache
"""
@functools.wraps(func)
def wrapped(*args, **kwargs):
args = tuple([frozendict(arg) if isinstance(arg, dict) else arg for arg in args])
kwargs = {k: frozendict(v) if isinstance(v, dict) else v for k, v in kwargs.items()}
return func(*args, **kwargs)
return wrapped
y entonces
@freezeargs
@lru_cache
def func(...):
pass
Código tomado de la respuesta de @fast_cen
(Sé que OP nolonger quiere una solución, pero vine aquí buscando la misma solución, por lo que lo dejo para las generaciones futuras)