【问题标题】:Python lru_cache false-negativesPython lru_cache 假阴性
【发布时间】:2017-10-11 15:29:39
【问题描述】:

我试图仅通过函数的第一个参数来缓存函数expand。出于缓存的目的,我不关心其他参数的值。

由于其他参数是 dicts,它们不可缓存,所以我定义了一个类来包含这些参数,其哈希值始终返回 0,因此缓存函数应该忽略它。

我在下面添加了一些缩减代码。我使用的是 Python 3.5.2 版。

class Node:
    def __init__(self, value):
        self.value = value

    def expand(self, a1, a2):
        return '{},{},{}'.format(self.value, a1, a2)


class ExpandArgs:
    def __init__(self, a1, a2):
        self.a1 = a1
        self.a2 = a2

    def __hash__(self):
        # We don't care about the hash, but it's required for caching
        return 0


@functools.lru_cache(maxsize=None)  # hash of args is always 0, so it should be ignored, and the hash of node should be used as the cache key
def expand(node, args):
    a1 = args.a1
    a2 = args.a2
    return node.expand(a1, a2)


e1 = ExpandArgs({}, {})
e2 = ExpandArgs({}, {})
print(hash(e1))  # 0
print(hash(e2))  # 0
node = Node(123)
print(expand.cache_info())  # CacheInfo(hits=0, misses=0, maxsize=None, currsize=0)
expand(node, e1)
print(expand.cache_info())  # CacheInfo(hits=0, misses=1, maxsize=None, currsize=1)
expand(node, e2)
print(expand.cache_info())  # CacheInfo(hits=0, misses=2, maxsize=None, currsize=2)
expand(node, e1)
print(expand.cache_info())  # CacheInfo(hits=1, misses=2, maxsize=None, currsize=2)
expand(node, e2)
print(expand.cache_info())  # CacheInfo(hits=2, misses=2, maxsize=None, currsize=2)

由于hash(e1) == hash(e2),我预计对expand() 的第二次调用会命中e1 的缓存值,但它没有命中。

为什么上述代码没有 1 次缓存未命中和 3 次缓存命中?

【问题讨论】:

标签: python caching functools


【解决方案1】:

原来使用 eq 而不是 hash 来检查函数参数是否相等以进行缓存,因此当我更改类时它可以工作。

class ExpandArgs:
    def __init__(self, context, forecast_transaction_node_map, date_range_func):
        self.context = context
        self.forecast_transaction_node_map = forecast_transaction_node_map
        self.date_range_func = date_range_func

    def __hash__(self):
        # We don't care about the hash, but it's required for caching
        return 0

    def __eq__(self, other):
        return isinstance(other, self.__class__)

【讨论】:

    【解决方案2】:

    我只是想在这方面添加一些注释,因为我花了一些时间阅读functools source code,试图弄清楚为什么__eq__在这里被使用。

    事实证明,这是用作缓存的 Python 字典的一个基本功能(在 python 3.9 上测试): 首先使用__hash__,但在哈希匹配时使用__eq__,以确保对象实际上不同:

    In [5]: class CompTuple(tuple):
       ...:     """Tuple that prints whenever equality operator is
       ...:  used"""
       ...:
       ...:     __hash__ = tuple.__hash__
       ...:
       ...:     def __eq__(self, other):
       ...:         print("equality comparison")
       ...:         return super().__eq__(other)
       ...:
    
    In [6]: t1 = CompTuple( (1,2,3,) )
    
    In [7]: t2 = CompTuple( (1,2,4,) )  # has different hash than t1
    
    In [8]: t3 = CompTuple( (1,2,3,) )  # has same hash as t1
    
    In [9]: d={}
    
    In [10]: d[t1]=1
    
    In [11]: d[t2]
    --------------------------------------------------------------
    KeyError                     Traceback (most recent call last)
    <ipython-input-11-c1f54cc7c51f> in <module>
    ----> 1 d[t2]
    
    KeyError: (1, 2, 4)
    
    In [12]: d[t3]
    equality comparison  # equality comparison because of same hash
    Out[12]: 1
    
    In [13]: d[t2]=2
    
    In [14]: d[t3]       # still only one equality comparison
    equality comparison
    Out[14]: 1
    

    python 文档explicitly require 比较相等的对象具有相同的哈希值。

    但是,调用__eq__ 可能比调用__hash__ 更昂贵。当缓存已经包含相同散列的对象时,这可能会产生反直觉的效果,使lru_cache 查找变得昂贵。

    附:只是为了让事情变得更加混乱,字典查找中有一个快捷方式跳过在比较相同对象时对__eq__的调用(=具有相同@987654332的对象@):

    In [15]: d[t1]  # no equality comparison, t1 *is* in the cache
    Out[15]: 1
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-02-03
      • 1970-01-01
      • 2015-09-28
      • 1970-01-01
      • 2018-04-22
      相关资源
      最近更新 更多