Python Lru_缓存(来自functools)是如何工作的?

Python Lru_缓存(来自functools)是如何工作的?,python,python-3.x,numpy,caching,lru,Python,Python 3.x,Numpy,Caching,Lru,尤其是使用递归代码时,lru\u缓存有了巨大的改进。我知道缓存是一种存储数据的空间,这些数据必须快速提供,并避免计算机重新计算 functools的Pythonlru缓存如何在内部工作 我在寻找一个具体的答案,它是否像Python的其他部分一样使用字典?它是否只存储返回值值 我知道Python很大程度上是建立在字典之上的,但是,我找不到这个问题的具体答案。希望有人能简化StackOverflow上所有用户的答案。 lru\u cache使用\u lru\u cache\u包装器decorator

尤其是使用递归代码时,
lru\u缓存有了巨大的改进
。我知道缓存是一种存储数据的空间,这些数据必须快速提供,并避免计算机重新计算

functools的Python
lru缓存
如何在内部工作

我在寻找一个具体的答案,它是否像Python的其他部分一样使用字典?它是否只存储
返回值


我知道Python很大程度上是建立在字典之上的,但是,我找不到这个问题的具体答案。希望有人能简化StackOverflow上所有用户的答案。

lru\u cache
使用
\u lru\u cache\u包装器
decorator(带参数的python decorator模式),它在上下文中有一个
缓存
字典,在其中保存被调用函数的返回值(每个被修饰的函数都有自己的缓存dict)。字典键由参数的
\u make\u key
函数生成。在下面添加了一些粗体评论:

# ACCORDING TO PASSED maxsize ARGUMENT _lru_cache_wrapper
# DEFINES AND RETURNS ONE OF wrapper DECORATORS

def _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo):
    # Constants shared by all lru cache instances:
    sentinel = object()      # unique object used to signal cache misses

    cache = {}                                # RESULTS SAVES HERE
    cache_get = cache.get    # bound method to lookup a key or return None

    # ... maxsize is None:

    def wrapper(*args, **kwds):
        # Simple caching without ordering or size limit
        nonlocal hits, misses
        key = make_key(args, kwds, typed)     # BUILD A KEY FROM ARGUMENTS
        result = cache_get(key, sentinel)     # TRYING TO GET PREVIOUS CALLS RESULT
        if result is not sentinel:            # ALREADY CALLED WITH PASSED ARGS
            hits += 1
            return result                     # RETURN SAVED RESULT
                                              # WITHOUT ACTUALLY CALLING FUNCTION
        misses += 1
        result = user_function(*args, **kwds) # FUNCTION CALL - if cache[key] empty
        cache[key] = result                   # SAVE RESULT

        return result
    # ...

    return wrapper

您可以查看源代码

本质上,它使用两种数据结构,一种是将函数参数映射到其结果的字典,另一种是跟踪函数调用历史的链表

缓存基本上是使用以下内容实现的,这是很自然的

cache = {}
cache_get = cache.get
....
make_key = _make_key         # build a key from the function arguments
key = make_key(args, kwds, typed)
result = cache_get(key, sentinel)
更新链表的要点如下:

elif full:

    oldroot = root
    oldroot[KEY] = key
    oldroot[RESULT] = result

    # update the linked list to pop out the least recent function call information        
    root = oldroot[NEXT]
    oldkey = root[KEY]
    oldresult = root[RESULT]
    root[KEY] = root[RESULT] = None
    ......                    

LRU缓存的Python 3.9源代码:

Fib代码示例

@lru_cache(maxsize=2)
def fib(n):
    if n == 0:
        return 0
    if n == 1:
        return 1
    return fib(n - 1) + fib(n - 2)
LRU缓存装饰器检查一些基本情况,然后用包装器_LRU_Cache_包装器包装用户函数。在包装器内部,将项添加到缓存的逻辑、LRU逻辑(即将新项添加到循环队列)以及将项从循环队列中移除的逻辑都会发生

def lru_cache(maxsize=128, typed=False):
...
    if isinstance(maxsize, int):
        # Negative maxsize is treated as 0
        if maxsize < 0:
            maxsize = 0
    elif callable(maxsize) and isinstance(typed, bool):
        # The user_function was passed in directly via the maxsize argument
        user_function, maxsize = maxsize, 128
        wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
        wrapper.cache_parameters = lambda : {'maxsize': maxsize, 'typed': typed}
        return update_wrapper(wrapper, user_function)
    elif maxsize is not None:
        raise TypeError(
         'Expected first argument to be an integer, a callable, or None')

    def decorating_function(user_function):
        wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
        wrapper.cache_parameters = lambda : {'maxsize': maxsize, 'typed': typed}
        return update_wrapper(wrapper, user_function)

    return decorating_function
  • 包装器在执行任何操作之前获取锁

  • 几个重要的变量-根列表包含符合
    maxsize
    value的所有项目。要记住root的重要概念是在上一个(0)和下一个位置(1)中自引用自身
    (root[:]=[root,root,None,None])

  • 三次高层检查
    • 第一种情况是,当
      maxsize
      为0时,这意味着没有缓存功能,包装器包装用户函数而没有任何缓存功能。包装器递增缓存未命中计数并返回结果

       def wrapper(*args, **kwds):
           # No caching -- just a statistics update
           nonlocal misses
           misses += 1
           result = user_function(*args, **kwds)
           return result
      
       def wrapper(*args, **kwds):
           # Simple caching without ordering or size limit
           nonlocal hits, misses
           key = make_key(args, kwds, typed)
           result = cache_get(key, sentinel)
           if result is not sentinel:
               hits += 1
               return result
           misses += 1
           result = user_function(*args, **kwds)
           cache[key] = result
           return result
      
    • 第二种情况。当
      maxsize
      为无时。在本节中,缓存中存储的元素数量没有限制。因此包装器检查缓存(字典)中的密钥。当密钥存在时,包装器返回值并更新缓存命中信息。当密钥丢失时,包装器使用用户传递的参数调用user函数,更新缓存,更新缓存未命中信息,并返回结果

       def wrapper(*args, **kwds):
           # No caching -- just a statistics update
           nonlocal misses
           misses += 1
           result = user_function(*args, **kwds)
           return result
      
       def wrapper(*args, **kwds):
           # Simple caching without ordering or size limit
           nonlocal hits, misses
           key = make_key(args, kwds, typed)
           result = cache_get(key, sentinel)
           if result is not sentinel:
               hits += 1
               return result
           misses += 1
           result = user_function(*args, **kwds)
           cache[key] = result
           return result
      
    • 第三种情况是,
      maxsize
      是默认值(128)或用户传递的整数值。下面是实际的LRU缓存实现。以线程安全的方式将整个代码保存在包装器中。在执行任何操作之前,请从缓存中读取/写入/删除

    LRU缓存
    • 缓存中的值存储为四项列表(记住根)。第一项是对前一项的引用,第二项是对下一项的引用,第三项是特定函数调用的键,第四项是结果。下面是斐波那契函数参数1的实际值
      [[…]、[…]、1、1]、[…]、[…]、1、1]、无、无]
      。[…]指对自我(列表)的引用

    • 第一个检查是缓存命中。如果是,缓存中的值是四个值的列表

       nonlocal root, hits, misses, full
       key = make_key(args, kwds, typed)
       with lock:
           link = cache_get(key)
            if link is not None:
                # Move the link to the front of the circular queue
                print(f'Cache hit for {key}, {root}')
                link_prev, link_next, _key, result = link
                link_prev[NEXT] = link_next
                link_next[PREV] = link_prev
                last = root[PREV]
                last[NEXT] = root[PREV] = link
                link[PREV] = last
                link[NEXT] = root
                hits += 1
                return result
      
      当项目已在缓存中时,无需检查循环队列是否已满或从缓存中弹出项目。而是更改循环队列中项目的位置。由于最近使用的项目始终位于顶部,因此代码将移动到队列顶部的最近值,并且上一个顶部项目将成为当前项目的下一个
      last[next]=root[PREV]=link
      link[PREV]=last
      link[next]=root
      。“下一步”和“上一步”在顶部初始化,指向链接字段的“上一步、下一步、键、结果=0、1、2、3”名称列表中的适当位置。最后,增加缓存命中信息并返回结果

       def wrapper(*args, **kwds):
           # No caching -- just a statistics update
           nonlocal misses
           misses += 1
           result = user_function(*args, **kwds)
           return result
      
       def wrapper(*args, **kwds):
           # Simple caching without ordering or size limit
           nonlocal hits, misses
           key = make_key(args, kwds, typed)
           result = cache_get(key, sentinel)
           if result is not sentinel:
               hits += 1
               return result
           misses += 1
           result = user_function(*args, **kwds)
           cache[key] = result
           return result
      
    • 当为缓存未命中时,更新未命中信息,代码检查三种情况。这三个操作都是在获得RLock后进行的。源代码中的三种情况按以下顺序排列-在缓存中找到锁密钥后,缓存已满,并且缓存可以接收新项。为了演示,让我们按照以下顺序操作:当缓存未满时,缓存已满,并且在获取锁后,密钥在缓存中可用

    当缓存未满时
    • 当缓存未满时,准备最近的
      结果(link=[last,root,key,result])
      以包含根以前的引用、根、键和计算结果

    • 然后将最近的结果(link)指向循环队列的顶部(
      root[PREV]=link
      ),root的上一个项目的next to指向最近的结果(
      last[next]=link
      ),并将最近的结果添加到缓存中(
      cache[key]=link

    • 最后,检查缓存是否已满(
      cache\u len()>=maxsize和cache\u len=cache.\uu len\uuuuuuuuuuuuuuuuuuuu)并将状态设置为“已满”

    • 对于fib示例,当函数接收到第一个值
      1
      时,root为空,root值为
      […]、[…]、None、None]
      且在