1. 简介
函数返回值缓存是优化一个函数的常用手段。我们可以将函数、输入参数、返回值全部保存起来,当下次以同样的参数调用这个函数时,直接使用存储的结果作为返回(不需要重新计算)。
这种方法是有代价的,我们实际是在用内存空间换取运行时间。如果所需要的存储空间不是很大的话,还是非常值得的。
2. 使用场景
这种优化方法的典型使用场景是在处理固定参数的函数被重复调用时。这样做可以确保每次函数被调用时,直接返回缓存的结果。如果函数被调用很多次,但是参数却一直变化,那么用这种方式缓存就没有什么效果了。
3. 例子一:优化递归的斐波那契数列
Code Example:
from functools import wraps
def fib_direct(n):
assert n > 0, 'invalid n'
if n < 3:
return n
else:
return fib_direct(n - 1) + fib_direct(n - 2)
def cache(func):
caches = {} # 使用dict来缓存
@wraps(func)
def wrap(*args):
if args not in caches:
caches[args] = func(*args)
return caches[args]
return wrap
@cache
def fib_cache(n):
assert n > 0, 'invalid n'
if n < 3:
return 1
else:
return fib_cache(n - 1) + fib_cache(n - 2)
if __name__ == "__main__":
from timeit import Timer
t1 = Timer("fib_direct(10)", "from __main__ import fib_direct")
t2 = Timer("fib_cache(10)", "from __main__ import fib_cache")
print t1.timeit(100000)
print t2.timeit(100000)
Output:
1.71311998367
0.0279998779297
递归时,我们要计算fib_cache(5)
就需要计算fib_cache(4)
和fib_cache(3)
,要计算fib_cache(4)
就需要计算fib_cache(3)
和fib_cache(2)
。注意如果不缓存的话,这里fib_cache(3)
会被计算两次。它符合我们进行此优化的条件。
4. 例子二
我们要重新计算一个元素很多的list中的每个元素,(实际上相当于一个map操作)。这个list有个特征就是重复元素非常多。
Code Example:
# -*- coding: utf-8 -*-
from functools import wraps
import numpy as np
import datetime
__author__ = 'BrownWong'
def direct_compu(x):
return x+100*30/2
def cache(func):
cache_dict = {}
@wraps(func)
def wrap(value):
key = value # 函数参数作为key(请保证用参数构成的key可哈希)
if key not in cache_dict:
cache_dict[key] = func(value)
print('cache')
return cache_dict[key]
return wrap
@cache
def cached_compu(x):
return x+100*30/2
if __name__ == '__main__':
num_list = np.random.randint(1, 5, int(1e7))
begin_time1 = datetime.datetime.now()
result1 = [direct_compu(num) for num in num_list]
end_time1 = datetime.datetime.now()
begin_time2 = datetime.datetime.now()
result2 = [cached_compu(num) for num in num_list]
end_time2 = datetime.datetime.now()
print('result1 time: %s' % (end_time1-begin_time1))
print('result2 time: %s' % (end_time2-begin_time2))
Output:
cache
cache
cache
cache
result1 time: 0:00:30.836147
result2 time: 0:00:04.240243
原始list中只有4个不重复元素,如果直接进行”map”,因为数据量很大,就很浪费时间。所以我们可以缓存。看输出结果,我们可以知道cache_dic
中只缓存了4个结果;你还能明显感觉到时间由原来的30多秒降低至4秒。
5. 剖析python中的实现
Python中实现函数返回值缓存是通过装饰器实现的。前面说过,要实现此类缓存,我们需要将函数、参数、返回值都缓存起来,那么在Python中是如何实现的呢?
- 函数的缓存是通过闭包实例来实现的,对于每一个被加上cache装饰器的函数,运行时都会被创建一个不同的闭包实例。
- 参数的缓存是通过闭包实例中引入的哈希表(dict)的key来保存的。key由参数生成。4中例子是直接使用参数作为key,这并不绝对。比如多参数情况下,你用这多个参数构成一个可哈希的key即可。
- 返回值的缓存是通过dict的value存储的。
6. 使用Python提供的缓存功能
functools模块和cachetools模块都提供了类似的缓存机制。
functools提供了lru_cache,如果缓存数据超出参数maxsize
的值,就用LRU(最少最近使用算法)清除最近很少使用过的缓存结果
Code Example:
@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)
而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.)
Ref