Использование @ functools.lru_cache со словарными аргументами - PullRequest
21 голосов
/ 15 июня 2011

У меня есть метод, который принимает (среди прочего) словарь в качестве аргумента. Этот метод выполняет синтаксический анализ строк, а словарь обеспечивает замену некоторых подстрок, поэтому он не должен быть изменяемым.

Эта функция вызывается довольно часто, и для избыточных элементов, поэтому я решил, что ее кэширование повысит ее эффективность.

Но, как вы уже догадались, поскольку dict является изменяемым и, следовательно, не хэшируемым, @functools.lru_cache не может украсить мою функцию. Так как я могу преодолеть это?

Бонус, если ему нужны только стандартные библиотечные классы и методы. В идеале, если бы в стандартной библиотеке было что-то вроде frozendict, которого я не видел, это сделало бы мой день.

PS: namedtuple только в крайнем случае, поскольку для этого потребуется большой синтаксический сдвиг.

Ответы [ 6 ]

9 голосов
/ 15 июня 2011

А как насчет создания хэш-класса dict, например:

class HDict(dict):
    def __hash__(self):
        return hash(frozenset(self.items()))

substs = HDict({'foo': 'bar', 'baz': 'quz'})
cache = {substs: True}
8 голосов
/ 27 июня 2017

Вот декоратор, использующий трюк @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

Просто добавьте его перед lru_cache.

@hash_dict
@functools.lru_cache()
def your_function():
    ...
4 голосов
/ 20 ноября 2018

Вместо использования настраиваемого хешируемого словаря, используйте его и не изобретайте колесо заново! Это замороженный словарь, который можно хэшировать.

https://pypi.org/project/frozendict/

Код:

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

, а затем

@freezeargs
@lru_cache
def func(...):
    pass

Код взят из ответа @fast_cen

Примечание: это не работает на рекурсивных структурах данных; например, у вас может быть аргумент, который является списком, который не подлежит изменению. Вам предлагается сделать обертку рекурсивной, чтобы она углублялась в структуру данных и делала каждый dict замороженным и каждый list кортеж.

(я знаю, что OP nolonger хочет решение, но я пришел сюда в поисках того же решения, поэтому оставляю его для будущих поколений)

2 голосов
/ 06 декабря 2016

Вот декоратор, который можно использовать как functools.lru_cache.Но это нацелено на функции, которые принимают только один аргумент , который является плоским отображением с хешируемыми значениями и имеет фиксированные maxsize 64. Для вашего использования-при случае вам придется адаптировать либо этот пример, либо ваш клиентский код.Кроме того, чтобы установить maxsize индивидуально, нужно было реализовать другой декоратор, но я не обдумывал это, потому что он мне не нужен.

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

Для более общего подходаможет использовать декоратор @cachetools.cache из сторонней библиотеки с соответствующей функцией, установленной как key.

2 голосов
/ 15 июня 2011

Как насчет подклассов namedtuple и добавления доступа по 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)
0 голосов
/ 05 октября 2017

После того, как мы решили пока отказаться от кэша lru для нашего варианта использования, мы все еще придумали решение.Этот декоратор использует json для сериализации и десериализации args / kwargs, отправляемых в кеш.Работает с любым количеством аргументов.Используйте его как декоратор для функции вместо @lru_cache.Максимальный размер установлен на 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...