此次的參考資料寫在前面,由於寫得真不錯!開始閱讀本篇分享前,建議先閱讀參考資料,若是還不能實現定時緩存裝飾器,再繼續從這裏開始讀。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