python缓存机制与functools.lru_cache

概述

前阵子看了下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启动流程源码解读

 

知者不言,言者不知

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

木子林_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值