【问题标题】:Cache object instances with lru_cache and __hash__使用 lru_cache 和 __hash__ 缓存对象实例
【发布时间】:2021-12-17 09:43:49
【问题描述】:

我不明白functools.lru_cache 如何处理对象实例。 我假设该类必须提供一个__hash__ 方法。所以任何具有相同哈希的实例都应该hit缓存。

这是我的测试:

from functools import lru_cache

class Query:    
    def __init__(self, id: str):
        self.id = id

    def __hash__(self):
        return hash(self.id)

@lru_cache()
def fetch_item(item):
    return 'data'

o1 = Query(33)
o2 = Query(33)
o3 = 33

assert hash(o1) == hash(o2) == hash(o3)

fetch_item(o1)  # <-- expecting miss
fetch_item(o1)  # <-- expecting hit
fetch_item(o2)  # <-- expecting hit BUT get a miss !
fetch_item(o3)  # <-- expecting hit BUT get a miss !
fetch_item(o3)  # <-- expecting hit

info = fetch_item.cache_info()
print(info)

assert info.hits == 4
assert info.misses == 1
assert info.currsize == 1

如何缓存具有相同哈希的对象实例的调用?

【问题讨论】:

    标签: python caching functools


    【解决方案1】:

    简答:为了在o1已经在缓存中的情况下对o2进行缓存命中,该类可以定义一个__eq__()方法,比较Query是否对象具有相同的价值。

    例如:

    def __eq__(self, other):
        return isinstance(other, Query) and self.id == other.id
    

    更新:另外一个细节值得在摘要中提及,而不是埋在细节中:此处描述的行为也适用于 Python 3.9 中引入的 functools.cache 包装器,因为 @cache() 是只是@lru_cache(maxsize=None) 的快捷方式。

    长答案(包括o3

    有一个很好的解释here 关于字典查找的确切机制,所以我不会重新创建它。可以这么说,由于 LRU 缓存存储为 dict,由于字典键的比较方式,类对象需要比较相等才能被视为已经存在于缓存中。

    您可以在一个普通字典的快速示例中看到这一点,该类有两个版本,一个使用__eq__(),另一个不使用:

    >>> o1 = Query_with_eq(33)
    >>> o2 = Query_with_eq(33)
    >>> {o1: 1, o2: 2}
    {<__main__.Query_with_eq object at 0x6fffffea9430>: 2}
    

    这会导致字典中有一项,因为键是相等的,而:

    >>> o1 = Query_without_eq(33)
    >>> o2 = Query_without_eq(33)
    >>> {o1: 1, o2: 2}
    {<__main__.Query_without_eq object at 0x6fffffea9cd0>: 1, <__main__.Query_without_eq object at 0x6fffffea9c70>: 2}
    

    导致两个项目(不相等的键)。

    Query 对象存在时,为什么int 不会导致缓存命中

    o3 是一个普通的int 对象。虽然它的值比较等于 Query(33),但假设 Query.__eq__() 正确比较类型,lru_cache 具有绕过该比较的优化。

    通常,lru_cache 会为包装函数的参数创建一个字典键(作为tuple)。可选地,如果缓存是使用 typed=True 参数创建的,它还存储每个参数的类型,以便只有当它们也是相同类型时值才相等。

    优化的是,如果包装函数只有一个参数,并且它的类型为intstr,则直接将单个参数用作字典键,而不是变成元组。因此,(Query(33),)33 不会比较相等,即使它们实际上存储了相同的值。 (请注意,我并不是说int 对象没有被缓存,只是它们与非int 类型的现有值不匹配。从您的示例中,您可以看到fetch_item(o3) 得到一个第二次调用时缓存命中)。

    如果参数的类型不同于int,您可以获得缓存命中。例如,33.0 将匹配,再次假定 Query.__eq__() 将类型考虑在内并返回 True。为此,您可以执行以下操作:

    def __eq__(self, other):
        if isinstance(other, Query):
            return self.id == other.id
        else:
            return self.id == other
    

    【讨论】:

      【解决方案2】:

      尽管lru_cache() 期望它的参数是可散列的,但它不使用它们的实际散列值,因此你会得到那些未命中的。

      函数_make_key 使 使用_HashedSeq 来确保它拥有的所有项目都是可散列的,但后来在_lru_cache_wrapper 中它不使用散列值。

      (_HashedSeq 如果只有一个参数,则跳过intstr 类型)

      class _HashedSeq(list):
          """ This class guarantees that hash() will be called no more than once
              per element.  This is important because the lru_cache() will hash
              the key multiple times on a cache miss.
          """
      
          __slots__ = 'hashvalue'
      
          def __init__(self, tup, hash=hash):
              self[:] = tup
              self.hashvalue = hash(tup)
      
          def __hash__(self):
              return self.hashvalue
      
      fetch_item(o1)  # Stores (o1,) in cache dictionary, but misses and stores (o1,)
      fetch_item(o1)  # Finds (o1,) in cache dictionary
      fetch_item(o2)  # Looks for (o2,) in cache dictionary, but misses and stores (o2,)
      fetch_item(o3)  # Looks for (o3,) in cache dictionary, but misses and stores (33,)
      

      不幸的是,没有提供自定义 make_key 函数的记录方法,因此,实现此目的的一种方法是通过猴子修补 _make_key 函数(在上下文管理器中):

      import functools
      from contextlib import contextmanager
      
      
      def make_key(*args, **kwargs):
          return hash(args[0][0])
      
      
      def fetch_item(item):
          return 'data'
      
      @contextmanager
      def lru_cached_fetch_item():
          try:
              _make_key_og = functools._make_key
              functools._make_key = make_key
              yield functools.lru_cache()(fetch_item)
          finally:
              functools._make_key = _make_key_og
      
      
      class Query:    
          def __init__(self, id: int):
              self.id = id
      
          def __hash__(self):
              return hash(self.id)
      
      
      o1 = Query(33)
      o2 = Query(33)
      o3 = 33
      
      assert hash(o1) == hash(o2) == hash(o3)
      
      with lru_cached_fetch_item() as func:
          func(o1)  # <-- expecting miss
          func(o1)  # <-- expecting hit
          func(o2)  # <-- expecting hit BUT get a miss !
          func(o3)  # <-- expecting hit BUT get a miss !
          func(o3)  # <-- expecting hit
      
      info = func.cache_info()
      print(info) # CacheInfo(hits=4, misses=1, maxsize=128, currsize=1)
      assert info.hits == 4
      assert info.misses == 1
      assert info.currsize == 1
      

      【讨论】:

        猜你喜欢
        • 2022-01-01
        • 2016-02-13
        • 2019-03-28
        • 2018-01-02
        • 2013-08-05
        • 2016-05-29
        • 1970-01-01
        • 2017-02-21
        相关资源
        最近更新 更多