Pregunta

Tengo un método que toma (entre otros) un diccionario como argumento. El método son las cadenas de análisis y el diccionario proporciona reemplazos para algunas sustros, por lo que no tiene que ser mutable.

Esta función se llama con bastante frecuencia, y en elementos redundantes, por lo 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 hash, @functools.lru_cache No puedo 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 lo he visto, me alegraría el día.

PD: namedtuple Solo en el último recurso, ya que necesitaría un gran cambio de sintaxis.

¿Fue útil?

Solución

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

Nota: Esto no funciona en datos recursivos; Por ejemplo, puede tener un argumento que es una lista, que no está a la altura. Está invitado a hacer que el envoltorio sea recursivo, de modo que se adentra en la estructura de datos y hace que cada dict congelado y cada list tupla.

(Sé que Op Nolonger quiere una solución, pero vine aquí buscando la misma solución, así que dejando esto para las generaciones futuras)

Otros consejos

¿Qué hay de crear un hashable? dict clase como así:

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 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():
    ...

¿Qué tal subclases? 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)

Aquí hay un decorador que se puede usar como functools.lru_cache. Pero esto se realiza en funciones que solo toman un argumento el cual es un mapeo plano con valores de hashable y tiene un fijo maxsize 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 envuelto la cabeza, 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, uno podría usar el decorador @cachetools.cache Desde una biblioteca de terceros con una función apropiada establecida como key.

Después de decidir lanzar el caché LRU para nuestro caso de uso por ahora, todavía se nos ocurrió una solución. Este decorador usa JSON para serializar y deserializar los args/kwargs enviados al caché. Funciona con cualquier número de args. Úselo como 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
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top