概述
前阵子看了下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
算法,其生命周期从进程创立后的被装饰函数的的第一次运行开始,直到进程结束。
我们可以知道下面这些结论:
- lru_cache使用闭包,实现函数结果的高速缓存
- 借助
LRU
算法(最近最少使用),实现函数结果的高速缓存更新。 - 如果
maxsize=None
,则将禁用LRU
功能,并且缓存可以无限增长。 - 如果将
typed
设置为true
,则将分别缓存不同类型的函数参数。例如,f(3)和f(3.0)将被视为具有不同结果的不同调用 - 由于使用字典来缓存结果,因此函数的位置和关键字参数必须是可哈希的。
- 可以将不同的参数模式视为具有单独的缓存项的不同调用。例如,
f(a=1,b=2)
和f(b=2,a=1)
的关键字参数顺序不同,并且可能具有两个单独的缓存条目。 func.cache_info():
查看缓存信息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 实现高速缓存及其原理 源码解析
知者不言,言者不知