python的标准库里有一个functools.lru_cache,性能不错,因为是用C语言实现的,智能,因为不需要设置缓存key,自动根据函数参数生成uuid。然而,却并不实用。理由有:
1. 居然不支持修饰async def函数,而async def函数一般比较耗时,反而更需要cache
2. 尽管很智能,但不支持指定key,有时候我们只需按某个参数(比如用户ID,URL)为key即可,而不需要它如此智能地将所有参数都考虑进去。能指定key,也会方便偶尔需要clear cache的场景
3. LRU算法虽然有用,但好像用到的机会非常少
所以需要自己实现一个cache修饰器,满足如下两点:
1. 无论def还是async def都能修饰
2. 可以指定缓存key
实现如下:
from operator import setitem
def cache(key='{}'):
def decorator(method):
_cache = {}
_waitable = asyncio.iscoroutinefunction(method)
method.cache_clear = lambda: _cache.clear()
@functools.wraps(method)
def wrapper(*args, **kwargs):
ckey = key.format(*args, **kwargs)
if ckey in _cache:
if _waitable:
f = asyncio.Future()
f.set_result(_cache[ckey])
return f
else:
return _cache[ckey]
res = method(*args, **kwargs)
if _waitable:
res = asyncio.get_event_loop().create_task(res)
res.add_done_callback(lambda x: setitem(_cache, ckey, x.result()))
else:
_cache[ckey] = res
return res
return wrapper
return decorator
特别说明:
1. 正如闭包的原理,_cache和_waitable被堆栈引用,不会被释放,所以无需挂载在任何函数或者全局变量下
2. 这里的key要求使用{}占位符表达式,这样使用str.format()格式化时不会报参数个数不匹配的错误
3. 据说官方之所以不支持的原因是asyncio.iscoroutinefunction()比较耗时,所以这里只调用了一次,并保存到闭包变量_waitable里
使用举例:
@cache('{}-{}')
async def fetch(schema, api, timeout):
return await GET(schema + '://' + DOMAIN + api, timeout=timeout)
await fetch('http', '/api/user/detail/', 6000)
await fetch('http', '/api/user/detail/', 12000)