python缓存模块的一些用法
Caches are bugs waiting to happen.- Rob Pike.
最近接触了几个关于python缓存模块的一些用法,记录一下。
###python3.2的@functools.lru_cache
functools 模块在python3.2以后新加入了这个装饰器,用来缓存一些耗时的IO请求结果或者周期调用的函数结果,如果缓存数据超出maxsize的值,就是用LRU(最近最少使用算法)清除一些缓存结果。
官方文档给出了几个示例:
@lru_cache(maxsize=32)
def get_pep(num):
'Retrieve text of a Python Enhancement Proposal'
resource = 'http://www.python.org/dev/peps/pep-%04d/' % num
try:
with urllib.request.urlopen(resource) as s:
return s.read()
except urllib.error.HTTPError:
return 'Not Found'
>>> for n in 8, 290, 308, 320, 8, 218, 320, 279, 289, 320, 9991:
... pep = get_pep(n)
... print(n, len(pep))
>>> get_pep.cache_info()
CacheInfo(hits=3, misses=8, maxsize=32, currsize=8)
还有最常见的斐波那契数列,没有优化过的递归算法时间复杂度是O(1.618 ^ n),如果用数组记录之前计算过的值或者用缓存就是O(n)的时间复杂度。
@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
>>> [fib(n) for n in range(16)]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]
>>> fib.cache_info()
CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)
###cachetools模块
cachetools模块帮我们实现了很多缓存失效方式,包括:
- LFUCache(Least Frequently Used (LFU) cache implementation.)
- LRUCache(Least Recently Used (LRU) cache implementation.)
- RRCache(Random Replacement (RR) cache implementation.)
- TTLCAche(LRU Cache implementation with per-item time-to-live (TTL) value.)
# pip install cachetools
import time
from cachetools import ttl_cache, cached
@ttl_cache(ttl=5)
def get(n):
print('from function') # 标记结果是从函数得到,而不是从缓存
return n
for i in range(3):
print(get(10))
time.sleep(6)
for i in range(3):
print(get(10))
可以看见结果输出如下:
from function
10
10
10
from function
10
10
10
###redis,memcached等内存数据库缓存
这个就不细说了,网上很多例子。可以使用数据库提供的python api操作数据库实现缓存,不过一般需要自己实现失效策略,也可以直接给redis设置超时过期参数使缓存失效。一般数据量比较大的时候考虑用内存数据库。
最近学习flask的时候发现werkzeug.contrib.cache模块实现了MemcachedCache和RedisCache,可以直接拿来用,实现参考源码。
###实现原理
一般就是设置一个字典,第一次计算的时候结果存入字典,以后每次就可以从字典里取得计算结果,如果需要超时就可以再设置一个字典保存对应结果计算的时期,每次计算比对这个时间,超时就重新计算。注意参数要是可以hash的,也就是可当做字典的key的。
def cache(func):
memo = {}
@wraps(func)
def _wrapper(*args):
res = memo.get(args, None)
if res is not None:
return res
else:
res = func(*args)
memo[args] = res
return res
return _wrapper
@cache
def fib(n):
if n <= 1:
return n
else:
return fib(n-1) + fib(n-2)
print([fib(i) for i in list(range(31))])
另外也可以用class来实现类似缓存,比如实现一个能够缓存网页爬虫结果的类:
import time
import requests
class CacheFetch:
__slots__ = ('url', 'timeout', 'last_time', '_response')
def __init__(self, url, timeout=None):
self.url = url
self.timeout = timeout or 3
self.last_time = time.time()
self._response = None
@property
def response(self):
if not self._response or (time.time()-self.last_time > self.timeout):
print('fetch page')
self._response = requests.get(self.url)
self.last_time = time.time()
return self._response
url = 'http://www.baidu.com'
fetcher = CacheFetch(url, 1)
for i in range(3):
st = time.time()
print(fetcher.response.status_code)
print(time.time()-st)
time.sleep(5)
for i in range(3):
st = time.time()
print(fetcher.response.status_code)
print(time.time()-st)
执行可以看到一开始的for循环第一次是请求的网站数据,后面两次是直接从缓存取得的。之间sleep一下,然后再一次的for循环第一次还是要请求原来的网页,因为已经超时了,后两次循环就是直接从缓存取了。
最近看别人的代码还见到了这个cache_property,从flask里边抠出来如下,思想也差不多:
class _Missing(object):
def __repr__(self):
return 'no value'
def __reduce__(self):
return '_missing'
_missing = _Missing() # 伪单例
class cached_property(object):
def __init__(self, func, name=None, doc=None):
self.__name__ = name or func.__name__
self.__module__ = func.__module__
self.__doc__ = doc or func.__doc__
self.func = func
def __get__(self, obj, type=None):
if obj is None:
return self
value = obj.__dict__.get(self.__name__, _missing)
if value is _missing:
value = self.func(obj)
obj.__dict__[self.__name__] = value
print(value)
return value
class TestCache():
@cached_property
def get(self):
print('from function')
return time.time()
def test():
t = TestCache()
print(t.get)
print(t.get)
test()
执行后可以看到get只计算了一次,后两个的time.time()函数并没有执行。所以缓存不适用这种每次都返回不同值的函数。
###Ref
https://wiki.python.org/moin/PythonDecoratorLibrary#Memoize - 总结了几乎所有python装饰器的用法
缓存算法
缓存更新的 Design Pattern,让我们多一点套路!