【问题标题】:Using @functools.lru_cache with dictionary arguments使用带有字典参数的@functools.lru_cache
【发布时间】:2020-10-02 19:17:54
【问题描述】:

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

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

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

如果它只需要标准库类和方法,则加分。理想情况下,如果它在标准库中存在某种我没见过的frozendict,那会让我很开心。

PS:namedtuple 只能在万不得已的情况下使用,因为它需要大量的语法转换。

【问题讨论】:

  • 也许这会有所帮助:stackoverflow.com/questions/4669391/…
  • 我没有看到这个,但它并没有真正的帮助。从头开始编写缓存装饰器不值得在这里付出努力,我想坚持使用标准库。还是谢谢你:)
  • 如何继承namedtuple 并通过x["key"] 添加访问权限?这可能只是几行代码。
  • 我知道获取命名元组的唯一方法是调用工厂collections.namedtuple,它返回一个type,所以如果我想将一个__getitem__添加到一个命名元组中,我会拥有动态地做到这一点,这是不可能的,即使它真的很丑陋。还有其他方法吗?
  • @Evpok:只需子类化namedtuple()返回的类型:class X(namedtuple("Y", "a b c")): ...

标签: python dictionary python-3.x hashable


【解决方案1】:

不要使用自定义哈希字典,而是使用它并避免重新发明轮子!这是一个完全可散列的冻结字典。

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 不再需要解决方案,但我来这里是为了寻找相同的解决方案,所以留给后代)

【讨论】:

  • @lru_cachelru_cache()?
【解决方案2】:

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

【讨论】:

  • 不允许你在装饰函数上调用clear_cache()cache_info()
  • 要提供clear_cache()cache_info() 调用,只需在返回前在wrapped 上添加这些函数即可。类似wrapper.cache_info = func.cache_infowrapper.cache_clear = func.cache_clear
【解决方案3】:

像这样创建一个可散列的dict 类怎么样:

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

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

【讨论】:

  • 工作就像一个魅力,虽然只有当 dict 的项目都是可散列的,但这是我的情况。但是,你有一些技巧来处理不可哈希的self.items()吗?
  • 我现在没有想到任何简单的方法。你当然可以递归字典并一路转换不可变(字典到frozensets,列表到元组等)......
【解决方案4】:

如何继承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)

【讨论】:

  • 也不错,但由于keys 是用户定义的,它会迫使我将其定义为调用方法的内部类,我想避免这种情况。不过,这个想法很棒,而且可能在某个时候有用。
【解决方案5】:

这是一个可以像functools.lru_cache 一样使用的装饰器。但这是针对仅采用 一个参数 的函数,该参数是一个 平面映射,具有 可散列值,并且具有 64 个固定的 maxsize。对于您的用例,您必须调整此示例或您的客户端代码。另外,要单独设置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

【讨论】:

    【解决方案6】:

    在决定暂时为我们的用例放弃 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
    

    【讨论】:

      【解决方案7】:

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

      【讨论】:

        猜你喜欢
        • 2022-01-25
        • 2017-09-04
        • 2017-06-03
        • 1970-01-01
        • 2023-02-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多