此次的参考资料写在前面,由于写得真不错!开始阅读本篇分享前,建议先阅读参考资料,若是还不能实现定时缓存装饰器,再继续从这里开始读。python
功能拆分:git
众所周知,python的functools
库中有lru_cache
用于构建缓存,而函数参数就是缓存的key
,所以,只要把缓存空间设置为1
,用时间值做为key
,便可实现定时执行函数。细节就去看参考资料2吧,我这里就不赘述了。
具体实现以下:github
""" 定时执行delay_cache """ import time from functools import lru_cache def test_func(): print('running test_func') return time.time() @lru_cache(maxsize=1) def delay_cache(_): return test_func() if __name__ == "__main__": for _ in range(10): print(delay_cache(time.time()//1)) # 1s time.sleep(0.2)
程序输出:缓存
running test_func 1582128027.6396878 1582128027.6396878 running test_func 1582128028.0404685 1582128028.0404685 1582128028.0404685 1582128028.0404685 1582128028.0404685 running test_func 1582128029.0425367 1582128029.0425367 1582128029.0425367
能够看到,test_func
在距上次调用1s
内直接输出缓存结果,调用间隔超过1s
时test_func
才会被真正执行。
手动实现缓存须要用字典,这里用lru_cache
装饰器代替了复杂的字典实现,就很优雅;-)app
装饰器的做用呢,就是给函数戴顶帽子,而后函数该干吗干吗去,然而别人看见的已经不是原来的函数,而是戴帽子的函数了。哈哈。函数
@delay_cache(time.time()//1) # (midori)帽子 def test_func(): print('running test_func') return time.time()
实现这个delay_cache
:测试
... import wrapt ... def delay_cache(t): @wrapt.decorator def wrapper(func, isinstance, args, kwargs): # 给func加缓存 @lru_cache(maxsize=1) def lru_wrapper(t): return func() return lru_wrapper(t) return wrapper ...
运行这段程序,就会获得错误的结果……(嘿嘿)code
test 1582129926.0 running test_func 1582129926.4459314 test 1582129926.0 running test_func 1582129926.6466658 test 1582129926.0 ...
能够看到,定时缓存好像消失了同样。缘由是装饰器返回的是wrapper
函数,而参数t
被wrapper
函数排除在外了。用print
打印t
,就会发现t
一直没有变。
等等,若是t
不变,那不该该是一直取缓存结果吗?ip
wrapper
函数返回的是lru_wrapper(t)
,是一个结果,而不是lru_wrapper
函数,因而可怜的lru_cache
跟着执行完的lru_wrapper
,被扔进了垃圾桶,今后被永远遗忘。等到下一次执行到这里,尽管新的t
相同,可是lru_cache
也是新的,它根本不记得本身曾经与t
还有过一段美好的姻缘过往……lru_wrapper
首次运行的时候把它存下来,后面的调用就全靠这个全局变量,而后输出结果就不变了。(要记得只须要在lru_wrapper
首次运行的时候把函数赋值给全局变量!)lru_cache
和t
隔世的姻缘,咱们的需求也不会实现,由于以前说过,参数t
被wrapper
函数排除在外了。若是不把t
做为装饰器的参数,而做为被装饰函数的参数呢?功能却是实现了,但是装饰器失去了它的价值,并且每一个用户函数,好比这里的test_func
,都要加上时间计算,变成test_func(time.time()//1, ...):
,到时候time
模块满天飞,难以直视,惨不忍睹。get
用类来作装饰器,类实例化之后就能够一直相伴lru_cache
左右,为它保驾护航。有关类装饰器的内容看参考资料1
class DelayCache(object): def __init__(self, delay_s): self.delay_s = delay_s @wrapt.decorator def __call__(self, func, isinstance, args, kwargs): self.func = func self.args, self.kwargs = args, kwargs hashable_arg = pickle.dumps((time.time()//self.delay_s, args, kwargs)) return self.delay_cache(hashable_arg) @lru_cache(maxsize=1) def delay_cache(self, _): return self.func(*self.args, **self.kwargs)
新的帽子作好了,给函数戴上试试看:
... @DelayCache(1) # 缓存 1s def test_func(_): print('running test_func') return time.time()
测试下效果:
if __name__ == "__main__": for _ in range(10): print(test_func(1)) # 只取定时缓存 time.sleep(0.2) # 测试结果: # running test_func # 首次运行定时不是设定的1s,下面给出解决方案 # 1582132259.4029999 # 1582132259.4029999 # 1582132259.4029999 # running test_func # 1582132260.0045283 # 1582132260.0045283 # 1582132260.0045283 # 1582132260.0045283 # 1582132260.0045283 # running test_func # 1582132261.0072334 # 1582132261.0072334
if __name__ == "__main__": for i in range(10): print(test_func(i)) # 每次都执行函数 time.sleep(0.2) # 测试结果: # running test_func # 1582132434.0865102 # running test_func # 1582132434.2869732 # running test_func # 1582132434.4875488 # ...
哈哈,这下终于搞定了。不过又冒出来2个问题:
首次运行的定时值并非1s
。
函数每次开始计时的时间点都是随机的,而缓存更新却依靠秒进位,因此首次运行的缓存时间多是0~1s
内任意一个时间点到1s
,因此不许。要解决这个问题,就要让时间从0
开始计时。个人作法是用一个self.start_time
属性记录函数首次运行的时间,而后计算实际间隔的时候,用取到的时间减去这个记录值,这样起始时间就必定从0
开始了。
参数改变的时候计时没有复位。
须要复位的地方就是执行delay_cache
的地方,因此在delay_cache
函数里复位计时值便可。
另外,每次复位后,(time.time() - self.start_time)
都从新从0
开始累加,(time.time() - self.start_time) // self.delay_s
的输出会变成...0,1,0,0,0,0,1,0,0,0,0,1,0,0...
,这样就不能做为lru_cache
的key
来断定了,因此添加一个self.tick
属性,把状态锁住,变成...0,0,1,1,1,1,1,0,0,0,0,0,1,1...
。
改动的地方直接看最终代码吧。
import time import pickle import wrapt from functools import lru_cache class DelayCache(object): def __init__(self, delay_s): self.delay_s = delay_s self.start_time = 0 self.tick = 0 @wrapt.decorator def __call__(self, func, instance, args, kwargs): self.func = func self.args, self.kwargs = args, kwargs if time.time() - self.start_time > self.delay_s: self.tick ^= 1 # 状态切换,至关于自锁开关 hashable_arg = pickle.dumps((self.tick, args, kwargs)) return self.delay_cache(hashable_arg) @lru_cache(maxsize=1) def delay_cache(self, _): self.start_time = time.time() # 计时复位 return self.func(*self.args, **self.kwargs) @DelayCache(delay_s=1) # 缓存1秒 def test_func(arg): print('running test_func') return arg if __name__ == "__main__": for i in [1, 1, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1]: print(test_func(i)) time.sleep(0.4)
用@wrapt.decorator
抵制套娃,用@lru_cache
干掉字典,代码变得异常清爽啊……
running test_func 1 1 running test_func 2 running test_func 3 running test_func 1 1 1 running test_func 1 1 1 running test_func 1 1