python缓存机制与functools.lru_cache

本文围绕 Python 的 functools.lru_cache 装饰器展开,介绍其在 Django 源码中的应用。阐述了缓存可加快数据获取速度,通过简单例子展示其缓存功能,对源码进行解析,还模拟实现缓存、动态展示过程,给出缓存到硬盘的例子,并推荐相关阅读。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

前阵子看了下django源码,在看到get_commads()方法时,看到了使用functools.lru_cache装饰器来实现缓存,现在我们简单讲解下。其实缓存是一种将定量数据加以保存以备迎合后续获取需求的处理方式,旨在加快数据获取的速度。数据的生成过程可能需要经过计算,规整,远程获取等操作,如果是同一份数据需要多次使用,每次都重新生成会大大浪费时间。所以,如果将计算或者远程请求等操作获得的数据缓存下来,会加快后续的数据获取需求。

简单例子

我们先看下下面简单的例子,来体验下functools.lru_cache装饰器

import time
from functools import lru_cache


@lru_cache()
def test_lru_cache(x, y):
    time.sleep(1)
    print('i am test')
    return x * 10, y * 10


print("第一次")
test_lru_cache(1, 2)
print("第二次")
test_lru_cache(1, 2)

运行结果:

第一次
i am test
第二次

从返回的现象我们可以看出来,test_lru_cache只执行了一次,从现象来看lru_cache装饰器的缓存功能起作用。

下面我们看下django是怎么实现的

源码解析

进来lru_cache方法,首先我们可以看到lru_cache就是一个闭包

def lru_cache(maxsize=128, typed=False):
    """
    最近最少使用的缓存装饰器。
    :param maxsize:数据大小(默认128), 如果maxsize=None,则将禁用LRU功能,并且缓存可以无限增长
    :param typed:如果将typed设置为true,则将分别缓存不同类型的函数参数
    :return:
    """
    # 用户仅应通过其公共API访问lru_cache:cache_info,cache_clear和f .__ wrapped__
    # lru_cache的内部结构被封装以确保线程安全并允许实现更改(包括可能的C版本)。
    # 早期检测到对@lru_cache的错误调用而没有任何参数,导致内部函数传递给maxsize而不是整数或None。
    if maxsize is not None and not isinstance(maxsize, int):
        raise TypeError('Expected maxsize to be an integer or None')

    def decorating_function(user_function):

        wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
        return update_wrapper(wrapper, user_function)

    return decorating_function

接下来我们进入到_lru_cache_wrapper方法看看

def _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo):
    """

    :param user_function:
    :param maxsize:
    :param typed: typed设置为true,则将分别缓存不同类型的函数参数
    :param _CacheInfo:
    :return:
    """
    # Constants shared by all lru cache instances:
    # 所有lru缓存实例共享的常量:
    # 表示缓存未命中的对象
    sentinel = object()
    # _make_key看起来是对key进行hash的功能
    make_key = _make_key   
    # link的字段
    PREV, NEXT, KEY, RESULT = 0, 1, 2, 3   
    # 存放缓存的字典
    cache = {}
    #命中和不命中
    hits = misses = 0
    #cache是否满了
    full = False
    # 从缓存字典中取值
    cache_get = cache.get
    # 缓存字典大小
    cache_len = cache.__len__
    #  因为链表更新不是线程安全的(保证线程安全)
    lock = RLock()
    # 循环双链表
    root = []
    # 通过指向自我来初始化
    root[:] = [root, root, None, None]

    if maxsize == 0:
        # 不缓存-成功调用后仅更新统计信息
        def wrapper(*args, **kwds):
            nonlocal misses
            result = user_function(*args, **kwds)
            misses += 1
            return result

    elif maxsize is None:
        # 简单的缓存,不限制大小
        def wrapper(*args, **kwds):
            nonlocal hits, misses
            # 返回key的hash值
            key = make_key(args, kwds, typed)
            # 缓存字典中取值
            result = cache_get(key, sentinel)
            # 记录没有命中的
            if result is not sentinel:
                hits += 1
                return result

            result = user_function(*args, **kwds)
            cache[key] = result
            misses += 1
            return result

    else:
        # 大小受限制的缓存,按最新的追踪数据
        def wrapper(*args, **kwds):
            nonlocal root, hits, misses, full
            key = make_key(args, kwds, typed)
            with lock:
                # 得到value
                link = cache_get(key)
                if link is not None:
                    # 将链接移到循环队列的最前面
                    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
            result = user_function(*args, **kwds)
            with lock:
                if key in cache:
                    # 到达此处表示该相同的key在释放锁定时已添加到缓存中。 由于Link更新已经完成,因此我们只需要返回计算结果并更新未命中计数
                    pass
                elif full:
                    # 使用旧的root 存储新的key和result。
                    oldroot = root
                    oldroot[KEY] = key
                    oldroot[RESULT] = result
                    # 清空最旧的Link,并使其成为新root。 保留对旧key和旧result的引用,以防止它们的引用计数在更新过程中变为零。
                    # 当我们仍在更新Link时,这将阻止运行潜在的任意对象清除代码(即__del__)。
                    root = oldroot[NEXT]
                    oldkey = root[KEY]
                    oldresult = root[RESULT]
                    root[KEY] = root[RESULT] = None
                    # 更新cache字典
                    del cache[oldkey]
                    # 在root和Link处于一致状态之后,将可能的可重入cache[key]分配保存到最后。
                    cache[key] = oldroot
                else:
                    # 将结果放在队列的最前面。
                    last = root[PREV]
                    link = [last, root, key, result]
                    last[NEXT] = root[PREV] = cache[key] = link
                    # 使用cache_len绑定方法代替len()函数
                    # 可能会包装在lru_cache本身中。
                    full = (cache_len() >= maxsize)
                misses += 1
            return result

    def cache_info():
        """cache字典信息"""
        with lock:
            return _CacheInfo(hits, misses, maxsize, cache_len())

    def cache_clear():
        """清空cache字典"""
        with lock:
            cache.clear()
            pass

    wrapper.cache_info = cache_info
    wrapper.cache_clear = cache_clear
    return wrapper

到目前为止,我们知道这个缓存方式是线程安全的,使用了LRU算法,其生命周期从进程创立后的被装饰函数的的第一次运行开始,直到进程结束。

我们可以知道下面这些结论:

  1. lru_cache使用闭包,实现函数结果的高速缓存
  2. 借助LRU算法(最近最少使用),实现函数结果的高速缓存更新。
  3. 如果maxsize=None,则将禁用LRU功能,并且缓存可以无限增长。
  4. 如果将typed设置为true,则将分别缓存不同类型的函数参数。例如,f(3)和f(3.0)将被视为具有不同结果的不同调用
  5. 由于使用字典来缓存结果,因此函数的位置和关键字参数必须是可哈希的。
  6. 可以将不同的参数模式视为具有单独的缓存项的不同调用。例如,f(a=1,b=2)f(b=2,a=1) 的关键字参数顺序不同,并且可能具有两个单独的缓存条目。
  7. func.cache_info():查看缓存信息
  8. func.cache_clear():清除缓存信息

另外想了解更彻底自己去django看下。

模拟实现缓存

import random
import datetime


class MyCache:

    def __init__(self):
        # 用字典结构以 kv 的形式缓存数据
        self.cache = {}
        # 限制缓存的大小
        self.max_cache_size = 10

    def __contains__(self, key):
        """
        判断该键是否存在于缓存
        :param key:
        :return: True or False
        """
        return key in self.cache

    def get(self, key):
        """从缓存中获取数据"""
        data = self.cache[key]
        data["date_accessed"] = datetime.datetime.now()
        return data["value"]

    def add(self, key, value):
        """
        添加数据(如果缓存过大删除最早的)
        :param key:
        :param value:
        :return:
        """
        if key not in self.cache and len(self.cache) >= self.max_cache_size:
            self.remove_oldest()
        self.cache[key] = {
            'date_accessed': datetime.datetime.now(),
            'value': value
        }

    def remove_oldest(self):
        """
        删除老数据
        :return:
        """
        oldest_entry = None

        for key in self.cache:
            if oldest_entry is None:
                oldest_entry = key
                continue
            curr_entry_date = self.cache[key]['date_accessed']
            oldest_entry_date = self.cache[oldest_entry]['date_accessed']
            if curr_entry_date < oldest_entry_date:
                oldest_entry = key

        self.cache.pop(oldest_entry)

    @property
    def size(self):
        """
        返回缓存容量大小
        :return:
        """
        return len(self.cache)


if __name__ == '__main__':
    # 测试缓存功能
    cache = MyCache()
    cache.add("test", sum(range(100000)))
    assert cache.get("test") == cache.get("test")

    keys = [
        'red', 'fox', 'fence', 'junk', 'other', 'alpha', 'bravo', 'cal',
        'devo', 'ele'
    ]
    s = 'abcdefghijklmnop'
    for i, key in enumerate(keys):
        if key in cache:
            continue
        else:
            value = ''.join([random.choice(s) for i in range(20)])
            cache.add(key, value)

    assert "test" not in cache
    print(cache.cache)

动态展示缓存过程

# 初始化

PREV, NEXT, KEY, RESULT = 0, 1, 2, 3
cache = {}
root = []
root[:] = [root, root, None, None] 

# 添加第一个缓存 
key= 1
result = 10
last = root[PREV]
link = [last, root, key, result]
last[NEXT] = root[PREV] = cache[key] = link

# 添加第二个缓存

key1 = 2
result1 = 20
last1 = root[PREV]
link1 =  [last, root, key1, result1]
last1[NEXT] = root[PREV] = cache[key1] = link1

# 重新运行第一次调用

link_prev, link_next, _key, result2 = 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

# 缓存已经到了最大值,最旧的缓存替换为新缓存
key2 = 3
result2 = 30
oldroot = root
oldroot[KEY] = key2
oldroot[RESULT] = result2
root = oldroot[NEXT]
oldkey = root[KEY]
oldresult = root[RESULT]
root[KEY] = root[RESULT] = None
del cache[oldkey]
cache[key2] = oldroot

将上面的代码放在pythontutor展示运行的过程

缓存到硬盘的例子

import os
import uuid
import pickle
import shutil
import tempfile
from functools import wraps as func_wraps


class DiskCache(object):
    """缓存数据到磁盘"""

    _NAMESPACE = uuid.UUID("xxxxxxx-xxxxx-xxxxxxxxxxxx")

    def __init__(self, cache_path=None):
        """
        
        :param cache_path: 路径
        """
        if cache_path:
            self.cache_path = os.path.abspath(cache_path)
        else:
            self.cache_path = os.path.join(tempfile.gettempdir(), ".diskcache")

    def __call__(self, func):
        """返回一个包装后的函数

        如果磁盘中没有缓存,则调用函数获得结果并缓存后再返回
        如果磁盘中有缓存,则直接返回缓存的结果
        """
        @func_wraps(func)
        def wrapper(*args, **kw):
            params_uuid = uuid.uuid5(self._NAMESPACE, "-".join(map(str, (args, kw))))
            key = '{}-{}.cache'.format(func.__name__, str(params_uuid))
            cache_file = os.path.join(self.cache_path, key)

            if not os.path.exists(self.cache_path):
                os.makedirs(self.cache_path)

            try:
                with open(cache_file, 'rb') as f:
                    val = pickle.load(f)
            except Exception:
                val = func(*args, **kw)
                try:
                    with open(cache_file, 'wb') as f:
                        pickle.dump(val, f)
                except Exception:
                    pass
            return val
        return wrapper

    def clear(self, func_name):
        """清理指定函数调用的缓存"""
        for cache_file in os.listdir(self.cache_path):
            if cache_file.startswith(func_name + "-"):
                os.remove(os.path.join(self.cache_path, cache_file))

    def clear_all(self):
        """清理所有缓存"""
        if os.path.exists(self.cache_path):
            shutil.rmtree(self.cache_path)

推荐阅读

Python functools.lru_cache 实现高速缓存及其原理 源码解析

缓存替换策略

django启动流程源码解读

 

知者不言,言者不知

 

<think>我们正在处理一个关于functools.lru_cache的错误:`functools._lru_cache_wrapperobjecthasnoattribute'index'`根据引用[3]:用户提到在低版本Python(如2.7)中,functools模块没有lru_cache,因为它是从Python3.2引入的。但是,现在用户遇到的错误是`_lru_cache_wrapper`对象没有`index`属性,这通常发生在尝试访问缓存对象的属性时,而该对象并没有这个属性。分析:1.用户可能在代码中使用了`lru_cache`,然后尝试访问一个名为`index`的属性,但是`lru_cache`返回的包装器对象并没有这个属性。2.常见的情况是:用户可能错误地将缓存装饰器应用到了一个方法上,然后试图访问原始方法的属性,而包装器对象可能没有这些属性。引用[5]提到:装饰器会改变函数的性质,被装饰的函数实际上变成了一个`_lru_cache_wrapper`类型的对象。这个对象通常只提供缓存相关的功能(如`cache_info`、`cache_clear`),并不保留原始函数的所有属性。因此,错误原因可能是:被装饰的函数(或方法)原本有一个`index`属性,但在装饰后,这个属性在包装器对象上不可用。解决方案:为了保留原始函数的属性,可以使用`functools.wraps`装饰器,但是`lru_cache`本身在实现时已经使用了`functools.wraps`,所以它应该保留原始函数的大部分属性。然而,如果用户定义的属性(如`index`)需要保留,可能需要额外的处理。但是,在标准库的`lru_cache`实现中,它确实使用`wraps`复制了原始函数的`__name__`、`__doc__`等属性,但不会复制用户自定义的属性。因此,如果原始函数有一个`index`属性,在装饰后,这个属性将丢失。解决这个问题的一种方法是:在装饰之后,手动将属性赋值给包装器函数。但更优雅的方式是使用一个可以保留属性的装饰器,或者使用第三方库(如`wrapt`)来编写装饰器。不过,对于`lru_cache`,我们可以考虑使用另一种方法:在装饰之前,将原始函数的属性复制到装饰后的函数上。但是,由于`lru_cache`返回的是一个包装器,我们可以在装饰后重新设置属性。但是,请注意:在Python3.8及以上版本,`functools.lru_cache`有一个新的特性:它现在可以保留被装饰函数的属性。实际上,在Python3.8中,`lru_cache`被重新实现为使用`functools.update_wrapper`,它复制了原始函数的属性。因此,如果用户使用的是Python3.8+,那么被装饰的函数应该保留原始函数的属性。如果用户使用的是Python3.8以下的版本(但>=3.2),那么`lru_cache`不会保留用户自定义属性。因此,我们可以建议:1.升级到Python3.8或更高版本,以自动保留属性。2.如果无法升级,可以手动设置属性。例如:```pythonfromfunctoolsimportlru_cachedefmy_decorator(func):decorated=lru_cache()(func)decorated.index=func.index#手动复制属性returndecorated@my_decoratordefmy_func():passmy_func.index=42#或者这样设置```3.或者,使用一个包装类来替代装饰器,但这可能比较复杂。但是,用户报错信息中显示的是`functools._lru_cache_wrapperobject`,这表明他们直接访问的是被装饰的方法,而该对象没有`index`属性。另一种可能性:用户可能在类中使用了`lru_cache`装饰方法,然后尝试访问方法的属性。例如:```pythonclassMyClass:@lru_cache()defmy_method(self,x):returnxobj=MyClass()#错误地访问了方法的属性?print(obj.my_method.index)#这里会报错,因为obj.my_method是一个绑定方法,并且被装饰后变成了缓存包装器对象,没有index属性。```如果用户确实需要访问原始函数的属性,可以考虑在装饰之前将属性复制到函数上,然后装饰,或者通过特殊的方式访问(比如通过类而不是实例访问)。但是,根据错误信息,用户尝试访问的是`index`属性,这可能是用户自己定义的一个属性。我们需要更多的上下文来确定。考虑到用户提供的引用中,有关于相对导入和版本的问题(引用[4]和[3]),我们也要注意用户的环境可能比较复杂。建议步骤:1.确认Python版本:如果是Python2.x,则根本没有`lru_cache`,需要升级到Python3.x。如果是Python3.2-3.7,则存在但不保留自定义属性。2.检查代码:用户是否在装饰后的函数上访问了自定义属性?如果是,考虑在装饰后重新设置属性。3.如果`index`属性是函数的一部分,那么尝试在装饰后重新赋值。然而,用户的具体错误信息是:`functools._lru_cache_wrapperobjecthasnoattribute'index'`。所以,问题发生在访问`.index`时。可能的情况:-用户错误地认为被装饰的函数有`index`属性,但实际上没有。-用户可能想访问缓存的信息,比如缓存命中次数等,但使用了错误的属性名。缓存信息是通过`cache_info()`方法访问的,而不是属性。因此,如果用户是想获取缓存统计信息,应该使用`cache_info()`:```python@lru_cache()deffunc(x):returnxfunc(1)print(func.cache_info())#输出缓存信息```如果用户确实需要自定义属性,那么按照上述方法手动设置。总结解决方案:1.如果用户想访问缓存信息,请使用`cache_info()`方法。2.如果用户确实为函数定义了`index`属性,并且需要在装饰后访问,则需要在装饰后重新设置该属性(如上所述)。3.升级到Python3.8+,它自动保留原始函数的属性。注意:在类中装饰方法时,由于装饰器返回的是一个普通函数(在Python3.8之前),它不会作为描述符,因此可能不会自动绑定`self`。但是,`lru_cache`在装饰方法时在Python3.8之前也是可以工作的,因为它返回的是一个可调用对象,并且当方法被实例调用时,第一个参数(实例)会被正确传递。不过,属性问题仍然存在。因此,我们给出以下回答:</think>### 关于 `functools._lru_cache_wrapper object has no attribute 'index'` 错误的解答 #### 错误原因分析 1. **属性不存在** `functools.lru_cache` 装饰器返回的对象类型是 `_lru_cache_wrapper`,它**没有 `index` 属性**。标准 `lru_cache` 对象仅提供以下方法: - `cache_info()`:返回缓存命中统计(如命中次数、缓存大小等)[^5] - `cache_clear()`:清空缓存 - `__call__()`:执行函数调用 尝试访问未定义的属性(如 `index`)会引发 `AttributeError`。 2. **常见触发场景** - 被装饰的函数/方法**原本有自定义属性**(如 `func.index = 42`),但装饰器覆盖了原始函数,导致属性丢失: ```python @lru_cache def my_func(): pass my_func.index = 42 # 装饰后属性被覆盖! print(my_func.index) # 报错 ``` - **误认为缓存对象包含索引机制**,试图直接访问 `index`。 --- #### 解决方案 ##### ✅ 方法 1:访问正确的缓存属性 若目标是获取缓存信息,使用 `cache_info()` 而非 `index`: ```python from functools import lru_cache @lru_cache(maxsize=128) def compute(x): return x * 2 compute(10) # 调用函数 print(compute.cache_info()) # 正确:输出缓存统计 # 输出示例:CacheInfo(hits=0, misses=1, maxsize=128, currsize=1) ``` ##### ✅ 方法 2:保留自定义属性(Python 3.8+) Python 3.8 及以上版本中,`lru_cache` 会**自动保留被装饰函数的属性**。升级 Python 版本即可解决: ```python # Python ≥ 3.8 @lru_cache def my_func(): pass my_func.index = 42 print(my_func.index) # 正常输出 42 ``` ##### ✅ 方法 3:手动保留属性(Python < 3.8) 在旧版 Python 中,需**显式复制属性**: ```python from functools import lru_cache def custom_lru_cache(func): cached_func = lru_cache()(func) cached_func.index = func.index # 复制原始属性 return cached_func @custom_lru_cache def my_func(): pass my_func.index = 42 # 在装饰前设置属性 print(my_func.index) # 正常输出 42 ``` ##### ✅ 方法 4:避免属性覆盖 通过 **`__wrapped__` 访问原始函数**(适用于 Python 3.4+): ```python @lru_cache def my_func(): pass my_func.__wrapped__.index = 42 # 操作原始函数 print(my_func.__wrapped__.index) # 输出 42 ``` --- #### 根本预防措施 1. **检查 Python 版本** 确认版本 ≥ 3.2(`lru_cache` 最低要求): ```bash python --version ``` 若低于 3.2,需升级 Python 或使用替代缓存方案(如 `functools.cached_property`)。 2. **属性设计规范** - 避免向被 `lru_cache` 装饰的函数添加自定义属性。 - 如需扩展功能,改用**类封装**: ```python class CachedCalculator: def __init__(self): self.index = 42 # 属性存储在类实例中 @lru_cache def compute(self, x): return x * self.index ``` --- ### 相关问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木子林_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值