将@ functools.lru_cache与字典参数一起使用

时间:2011-06-15 13:32:06

标签: python dictionary python-3.x hashable

我有一个方法,其中(以及其他)字典作为参数。该方法是解析字符串,字典提供了一些子字符串的替换,因此它不必是可变的。

这个函数经常调用,而且在冗余元素上,所以我认为缓存它会提高效率。

但是,正如您可能已经猜到的那样,由于dict是可变的,因此不可清除,@functools.lru_cache无法修饰我的函数。那我怎么能克服这个呢?

如果它只需要标准的库类和方法,则为奖励点。理想情况下,如果它在标准库中存在某种frozendict,我还没有看到它会成为我的一天。

PS:namedtuple只是最后的手段,因为它需要大量的语法转换。

7 个答案:

答案 0 :(得分:9)

如何像这样创建一个可散列的dict类:

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

substs = HDict({'foo': 'bar', 'baz': 'quz'})
cache = {substs: True}

答案 1 :(得分:8)

这是一个使用@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():
    ...

答案 2 :(得分:2)

如何对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)

答案 3 :(得分:2)

这是一个可以像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

答案 4 :(得分:2)

与其使用自定义的可哈希字典,不如使用它,避免重新发明轮子!这是一本冻结的字典,都是可哈希的。

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的答案中获取的代码

(我知道OP不再需要解决方案,但我来这里是为了寻找相同的解决方案,因此将其留给后代使用)

答案 5 :(得分:0)

在决定暂时为我们的用例删除lru缓存之后,我们仍然提出了一个解决方案。此装饰器使用json序列化和反序列化发送到缓存的args / kwargs。适用于任意数量的args。将它用作函数的装饰器而不是@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

答案 6 :(得分:0)

基于@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