library python dictionary python-3.x hashable

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)