Использование @functools.lru_cache с аргументами словаря

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

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

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

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

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


person Evpok    schedule 15.06.2011    source источник
comment
Может быть, это может помочь: stackoverflow.com/questions/4669391/   -  person mouad    schedule 15.06.2011
comment
Я этого не видел, но это не особо помогает. Написание декоратора кеша с нуля не стоит усилий, и я хотел бы придерживаться стандартной библиотеки. В любом случае, спасибо :)   -  person Evpok    schedule 15.06.2011
comment
Как насчет подкласса namedtuple и добавления доступа x["key"]? Вероятно, это будет всего несколько строк кода.   -  person Sven Marnach    schedule 15.06.2011
comment
Единственный известный мне способ получить именованные кортежи — это вызвать фабрику collections.namedtuple, которая возвращает type, поэтому, если я хочу добавить __getitem__ к именованному кортежу, мне придется делать это динамически, что невозможно, и даже если это действительно некрасиво. Есть ли другой способ сделать это?   -  person Evpok    schedule 15.06.2011
comment
@Evpok: Просто создайте подкласс типа, возвращаемого namedtuple(): class X(namedtuple("Y", "a b c")): ....   -  person Sven Marnach    schedule 15.06.2011
comment
@Sven Marnach Не думал об этом. Хороший :)   -  person Evpok    schedule 15.06.2011


Ответы (7)


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

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 кортеж.

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

person Cedar    schedule 20.11.2018

Вот декоратор, который использует трюк @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():
    ...
person fast_cen    schedule 27.06.2017
comment
Не позволяет вам вызывать clear_cache() или cache_info() для декорированной функции. - person Nicholas Tulach; 04.10.2017
comment
Чтобы обеспечить вызовы clear_cache() и cache_info(), просто добавьте эти функции в wrapped перед возвратом. Что-то вроде wrapper.cache_info = func.cache_info и wrapper.cache_clear = func.cache_clear - person rcsalvador; 29.05.2018

Как насчет создания хешируемого класса dict вот так:

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

substs = HDict({'foo': 'bar', 'baz': 'quz'})
cache = {substs: True}
person mhyfritz    schedule 15.06.2011
comment
Работает как шарм, но только если все элементы dict являются хешируемыми, но это мой случай. Однако есть ли у вас какие-то хитрости для работы с нехешируемыми self.items()? - person Evpok; 15.06.2011
comment
Сейчас мне в голову не приходит легкий способ. Конечно, вы могли бы выполнить рекурсию вниз по диктовке и преобразовать неизменяемые по пути (диктовки в замороженные наборы, списки в кортежи и т. д.)... - person mhyfritz; 15.06.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)
person Sven Marnach    schedule 15.06.2011
comment
Это тоже хорошо, но поскольку keys должен определяться пользователем, это заставит меня определить это как внутренний класс для моего вызывающего метода, и я хочу этого избежать. Тем не менее, отличная идея, и, вероятно, когда-нибудь пригодится. - person Evpok; 15.06.2011

Вот декоратор, который можно использовать как 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.

person funky-future    schedule 06.12.2016

Решив пока отказаться от кэша 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
person Harel    schedule 05.10.2017

На основе @Cedar answer добавление рекурсии для глубокой заморозки, как было предложено:

def deep_freeze(thing):
    from collections.abc import Collection, Mapping, Hashable
    from frozendict import frozendict
    if thing is None or isinstance(thing, str):
        return thing
    elif isinstance(thing, Mapping):
        return frozendict({k: deep_freeze(v) for k, v in thing.items()})
    elif isinstance(thing, Collection):
        return tuple(deep_freeze(i) for i in thing)
    elif not isinstance(thing, Hashable):
        raise TypeError(f"unfreezable type: '{type(thing)}'")
    else:
        return thing


def deep_freeze_args(func):
    import functools

    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        return func(*deep_freeze(args), **deep_freeze(kwargs))
    return wrapped
person Boris Lopez    schedule 21.03.2021