A. Python性能提升神器!lru_cache的介紹和講解
我們經常談論的緩存一詞,更多的類似於將硬碟中的數據存放到內存中以至於提高讀取速度,比如常說的redis,就經常用來做數據的緩存。 Python的緩存(lru_cache)是一種裝飾在被執行的函數上,將其執行的結果緩存起來,當下次請求的時候,如果請求該函數的傳參未變則直接返回緩存起來的結果而不再執行函數的一種緩存裝飾器。
那它和redis的區別在哪?有什麼優勢?怎麼使用? 下面為你講解
1.現在我們先不使用緩存來寫一個求兩數之和的函數,並調用執行它兩次:
執行結果
可以看到 test 被執行了兩次,現在我們加上緩存再進行執行:
執行結果
可以看到 test 函數只被執行了一次,第二次的調用直接輸出了結果,使用了緩存起來的值。
2.當我們使用遞歸求斐波拉契數列 (斐波那契數列指的是這樣一個數列:0,1,1,2,3,5,8,它從第3項開始,每一項都等於前兩項之和) 的時候,緩存對性能的提升就尤其明顯了:
不使用緩存求第40項的斐波拉契數列
執行時間
使用緩存求第40項的斐波拉契數列:
執行時間
兩個差距是非常明顯的,因為不使用緩存時,相當於要重復執行了很多的函數,而使用了 lru_cache 則把之前執行的函數結果已經緩存了起來,就不需要再次執行了。
查看lru_cache源碼會發現它可以傳遞兩個參數: maxsize 、 typed :
代表被lru_cache裝飾的方法最大可緩存的結果數量 (被裝飾方法傳參不同一樣,則結果不一樣;如果傳參一樣則為同一個結果) , 如果不指定傳參則默認值為128,表示最多緩存128個返回結果,當達到了128個時,有新的結果要保存時,則會刪除最舊的那個結果。如果maxsize傳入為None則表示可以緩存無限個結果;
默認為false,代表不區分數據類型,如果設置為True,則會區分傳參類型進行緩存,官方是這樣描述的:
但在python3.9.8版本下進行測試,typed為false時,按照官方的測試方法測試得到的還是會被當成不同的結果處理,這個時候typed為false還是為true都會區別緩存,這與官方文檔的描述存在差異:
執行結果
但如果是多參數的情況下,則會被當成一個結果:
執行結果
這個時候設置typed為true時,則會區別緩存:
執行結果
當傳參個數大於1時,才符合官方的說法,不清楚是不是官方舉例有誤
當傳遞的參數是dict、list等的可變參數時,lru_cache是不支持的,會報錯:
報錯結果
緩存 緩存位置 是否支持可變參數 是否支持分布式 是否支持過期時間設置 支持的數據結構 需單獨安裝 redis 緩存在redis管理的內存中 是 是 是 支持5種數據結構 是 lru_cache 緩存在應用進程的內存中,應用被關閉則被清空 否 否 否 字典(參數為:key,結果為:value) 否
經過上面的分析,lru_cache 功能相對於redis來說要簡單許多,但使用起來更加方便,適用於小型的單體應用。如果涉及的緩存的數據種類比較多並且想更好的管理緩存、或者需要緩存數據有過期時間(類似登錄驗證的token)等,使用redis是優於lru_cache的。
B. python 緩存記憶化 @lru_cache()
使用孝手裝飾器lru_cache加速函數計算
lru是一種緩存淘汰演算法
(least recently used)即最近最少使用淘汰演算法
不用lru_cache
使用lru_cache
lru_cache的定義如下,
第一個參數maxsize控制最大緩存數量,
第二個參數為True則嚴枯粗格檢查被裝飾函數的參數類沒慎鎮型
C. python3_原生 LRU 緩存
原生 LRU 緩存(最低 Python 版本為 3.2)
目前,幾猜察乎所有層面上的軟體和硬體中都需要緩存。Python 3 將 LRU(最近最少使用演算法)緩存作為一個名為「lru_cache」的裝飾器,使得對緩存的使用非常簡單。
下面是一個簡單的斐波那契函數,我們知道使用緩存將有助於該函數的計算,因為它會通過遞歸多次執行相同的工遲兆唯作。
現在,我們可以使用「lru_cache」來優化它(這種優化技術被稱為「memoization」)。通過這種碼培優化,我們將執行時間從幾十秒降低到了幾秒。
D. python有緩存模塊嗎
從Python 3.2開始,可以使用functools庫中的裝飾器@lru_cache。這是最近使用過的緩存,所以其中的項目沒有到期時間,但作為快速入侵,它非常有用。
from functools import lru_cache
@lru_cache(maxsize=256)def f(x):
return x*xfor x in range(20):
print f(x)for x in range(20):
print f(x)
E. 設計一個LRU緩存淘汰演算法
從結構上來看,雙向鏈表可以支持O(1)時間復雜度的情況下找到前驅結點,正是這樣的特點,使得雙向鏈表在某些情況下的插入、刪除操作比單鏈表更加高效。
我們已經找到要刪除的結點,但是刪除某個結點q,需要其前驅結點,而單鏈表不支持直接獲得前驅結點,所以為了找到前驅結點,需要從頭開始遍歷鏈表,直到p<-next=q,說明p是q的前驅結點。對於雙向鏈表來說,這叢鍵種情況就比較有優勢嘩爛。因為雙向鏈表已經保存了前驅結點的指針,不需要像單鏈表那樣遍歷。單鏈表刪除操作需要O(n)的時間復雜度,而雙鏈表只需要O(1)的時間復雜度。
題目描述:
實現 LRUCache 類:
LRUCache(int capacity) 以正整數作為容量 capacity 初始化 LRU 緩存
int get(int key) 如果關鍵字 key 存在於緩存中,則返回關鍵字的值,否則返回 -1 。
void set(int key, int value) 如果關鍵字已經存在,則變更其數據值;如果關鍵字不存在,則插入該組「關鍵字-值」。當緩存容量達到上限時,它應該在寫入新數據之前刪除最久未使用的數據值滲蘆巧,從而為新的數據值留出空間。
示例:
輸入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
輸出
[null, null, null, 1, null, -1, null, -1, 3, 4]
解釋
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 緩存是 {1=1}
lRUCache.put(2, 2); // 緩存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 該操作會使得關鍵字 2 作廢,緩存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 該操作會使得關鍵字 1 作廢,緩存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
F. python函數可以提高代碼執行速度嗎
不同的編程語言,會有不同 的緩存策略,例如,通過哈希映射、優先順序隊列等實現緩存。因此,不同的編程語言,在緩存的解決方案方面具有很大差異,可能需要幾分鍾,也可能需要幾小時。
但是,在Python中,標准工具包functools實現了一種名為LRU(Least Recently Used)的緩存策略,可以通過傳入參數,來設定緩存最近多少次的計算結果,如果傳入參數為None,那麼則不緩存。
G. FIFO和LRU小結
一:FIFO演算法
1.0,FIFO (First in First out) 先進先出(核心原則:最先進來的,最先淘汰); 其實在操作系統的設計理念中很多地方都是利用到了先進先出的思想就是因為這個原則簡單切符合人們的慣性思維,具備公平性實現起來也簡單,直接使用數據結構中的隊列即可實現
1.1,在FIFO 中應該支持這些操作: 一,get(key),如果Cache中存在該key,則返回對應的value值,否則返回 -1; 二,set(key,value),如果Cache中存在該key,則重置value值,如果不存在則將該key插入到Cache中,若Cache已滿則淘汰最先進入Cache的數據
1.2,那麼利用什麼數據結構來實現呢?有這一種思路, 利用一個雙向鏈表保存數據,當來了新數據之後便添加到鏈表末尾,如果Cache存滿數據,則把鏈表頭部數據刪除,然後把次年數據添加到鏈表末尾,在訪問數據的時候,如果在Cache中存在該數據的話,則返回對應的value值,否則返回 -1,如果想提高訪問效率,可以利用hashmap來保存每個key在鏈表中的位置(參考下面拓展)
二:LRU演算法
1.0,LRU (Least recently used) 最近最久未使用調度,其核心思想是"如果數據最近被訪問過,那麼將來被訪問的幾率也更高"; 最常見的實現是使用一個鏈表保存緩存數據,詳細演算法實現如下:
1,新數據插入到鏈表頭部
2,每當緩存命中(即緩存數據被訪問),則將數據移到鏈表頭部;
3,當鏈表滿的時候,將鏈表尾部的數據丟棄
1.1,LRU的優缺點 1.命中率,當存在熱點數據時,LRU的效率很好,但偶發性的,周期性的批量操作會導致LRU命中率急劇下降,緩存污染情況比較嚴重 2,實現相對簡單 3,命中時需要遍歷鏈表,找到命中的數據塊索引,然後需要將數據移到頭部
2.0,LRU-K K代表最近使用的次數,因此LRU也可以認為是LRU-1,它主要是為了解決LRU演算法"緩存污染"的問題,其核心思想是將"最近使用過1次"的判斷標准擴展為"最近使用過K次"; 相比LRU要多維護一個隊列,用於記錄所有緩存數據被訪問的歷史,只有當數據的訪問次數達到K次的時候,才將數據放入緩存.當需要淘汰數據時,LRU-k會淘汰第K次訪問時間距離當前時間最大的數據.詳細實現如下:
2.1,一,數據第一次被訪問,加入到訪問歷史列表; 二,如果數據在訪問歷史列表裡後達到K次訪問,則按照一定(FIFO, LRU)淘汰; 三,當訪問歷史隊列中的數據訪問次數達到k次後,將數據l索引從歷史隊列刪除,將數據移到緩存隊列中,並緩存此數據,緩存隊列重新按照時間排序; 四,緩存數據隊列中被再次訪問後,重新排序; 五,需要淘汰數據時,淘汰緩存隊列中排在末尾的數據(即:淘汰倒數第K次訪問離現在最久的數據)
2.2,LRU-K具有LRU的優點,同時能夠避免LRU的缺點,實際應用中LRU-2是綜合各種因素後最優的選擇,LRU-3或者更大的K值命中率會高,但適應性差,需要大量的數據訪問才能將歷史記錄緩存或者清除掉
2.3優缺,LRU-K降低了"緩存污染"帶來的問題,命中率比LRU要高,但LRU-K隊列是一個優先順序隊列,演算法復雜度和代價相對LRU較高,並且LRU需要記錄那些被訪問過,但是沒有達到K次也就是還沒有放入緩存的對象,因此b內存消耗會比LRU要多,當然如果數據量很大的時候,內存消耗會比較可觀
3.0,Two queues (2Q) 演算法類似於LRU-2,不同點在於2Q將LRU-2演算法中的訪問歷史隊列(歷史隊列,還沒有緩存數據)改為一個FIFO緩存隊列,即: 2Q演算法有兩個緩存隊列,一個是FIFO隊列,一個是LRU隊列.
3.1,當數據第一次訪問時,2Q演算法會將數據緩存在FIFO隊列裡面,當數據第二次被訪問時,則將數據從FIFO隊列移到LRU隊列裡面,兩個隊列各自按照自己的方法淘汰數據; 一,新訪問的數據插入到FIFO隊列, 二,如果數據在FIFO隊列中一直沒有被再次訪問,則最終按照FOFO規則淘汰, 三,如果數據在FIFO隊列中被再次訪問,則將數據移到LRU隊列頭部, 四,如果數據在LRU隊列再次被訪問,則將數據移到LRU隊列頭部, 五,LRU隊列淘汰末尾的數據
3.2,可能會感覺FIFO隊列比LRU隊列短,但並不代表這是演算法的要求,實際應用中兩者比例沒有硬性要求
3.3,2Q演算法命中率高於LRU,切需要兩個隊列,但兩個隊列本身都比較簡單,代價是FIFO和LRU代價之和; 2Q演算法和LRU-2演算法命中率類似,內存消耗也比較接近,但對於最後的緩存數據來說,2Q減少一次從原始儲存讀取數據或者計算數據的操作
4.0,Multi Queue (MQ) 演算法根據訪問頻率將數據劃分為多個隊列,不同的隊列具有不同的訪問優先順序,其核心思想是:優先緩存訪問次數多的數據
4.1,MQ演算法將緩存劃分為多個LRU隊列,每個隊列對應不同的訪問優先順序,訪問優先順序是根據訪問次數計算出來的,詳情: 一,新插入的數據放入Q0; 二,每個隊列按照LRU管理數據; 三,當數據訪問次數達到一定次數需要提升優先順序時將數據從當前隊列刪除,加入到高一級的隊列頭部; 四,為了防止高優先順序數據永遠不被淘汰,每個隊列淘汰數據時,將數據從緩存中刪除,將數據加入Q-history頭部; 五,需要淘汰數據時,從最低一級隊列開始按照LRU淘汰,每個隊列淘汰數據時,將數據從緩存中刪除,將數據索引加入Q-history頭部; 六,如果數據在Q-history中被重新訪問,則重新計算其優先順序,移到目標隊列的頭部; 七,Q-history按照LRU淘汰數據的索引
4.2,MQ降低了"緩存污染"帶來的問題,命中率比LRU高,但MQ需要維護多個隊列,切需要維護每個數據的訪問時間,復雜度比較高,並且MQ需要記錄每個數據的訪問時間,需要定時掃碼所有隊列,代價也比較高
4.3,雖然MQ的隊列看起來數量比較多,但由於所有隊列之和受限於緩存容量的大小,因此這里多個隊列長度之和和一個LRU隊列是一樣的,因此隊列掃碼性能接近
小結: 命中率 LRU-2 > MQ(2) > 2Q > LRU ; 復雜度 LRU-2 > MQ(2) > 2Q >LRU ; 代價 LRU-2 > MQ(2) > 2Q > LRU ; 需要注意的是,命中率並不是越高越好,實際中應該根據業務需求和對數據的訪問情況進行分析選擇,LRU雖然看起來命中率低一些,切存在"緩存污染"的問題,但其簡單切代價小,實際中反而應用更多
拓展:基於 雙鏈表的 LRU 實現: 一,傳統意義的LRU演算法是每一個Cache對象設置一個定時器,每次Cache命中則給定時器 +1,而Cache用完需要淘汰舊內容,放置新內容時就查看所有的計時器,並將使用的內容替換掉; 其弊端很明顯,如果Cache的數量少,問題不大,但如果Cache的空間過大,達到10W或者100W以上,一旦需要淘汰,則需要遍歷所有計時器,其性能與資源消耗巨大,效率也就非常的慢了; 二,雙鏈表原理,將Cache的所有位置都用雙鏈表連接起來,當一個位置被命中之後,就將通過調整鏈表的指向,將該位置調整到鏈表頭的位置,新加入Cache直接加到鏈表頭中,這樣在多次進行Cache操作後,最近被命中的就會被向鏈表頭方向移動,而沒有命中的則向鏈表後部移動,鏈表尾則表示最近最少命中的Cache,當需要替換內容時我們只需要淘汰鏈表最後的部分即可!
如果錯誤或者建議,歡迎下方留言,謝謝!
H. 你可能沒有在Python3中使用但卻應該使用的東西
由於Python EOL的發布,許多人開始將他們的Python版本從2切換到3。不幸的是,我發現大多數Python3看起來仍然像Python2,但是要加括弧(盡管在我之前的文章《使用Python進行web抓取介紹》中的代碼示例也是這樣)。下面,我將展示一些令人興奮的特性示例,你只能在Python3中使用它們,希望它可以讓你使用Python解決問題變得更容易。
所有示例都是在Python 3.7中編寫的,每個特性都包含該特性所需的最低Python版本。
在任何編程語言中,沒有字元串是很難做任何事情的,為了保持理智,你希望有一種結構化的方法來處理字元串。大多數使用Python的人更喜歡使用format方法。
除了format,Python 3還提供了一種通過f-strings進行字元串插值的靈活方法。和上面一樣使用f-strings的代碼是這樣的:
f-strings非常棒,但是有些字元串(比如文件路徑)有自己的庫,這使得它們的操作更加容易。Python 3提供了pathlib作為一個處理文件路徑的方便抽象。如果你不確定為什麼你應該使用pathlib,試著閱讀這篇優秀的文章——《為什麼你應該使用pathlib》——Trey Hunner。
靜態和動態類型是軟體工程中一個熱門的話題,幾乎每個人都對此有自己的看法。我將讓讀者決定何時應該編寫類型,但我認為你至少應該知道Python 3支持類型提示。
Python 3提供了一種通過Enum類來編寫枚舉的簡單方法。枚舉是封裝常量列表的一種方便的方法,因此它們不會在沒有太多結構的情況下隨機分布在你的代碼中。
枚舉是一組符號名稱(成員),它們綁定到惟一的常量值。在枚舉中,可以通過標識對成員進行比較,並且枚舉本身也可以被遍歷。https://docs.python.org/3/library/enum.html
緩存存在於我們今天使用的幾乎所有水平的軟體和硬體中。Python 3通過將LRU(最近敏模最少使用的)緩存公開為一個名為lru_cache的裝飾器,使得使用它們變得非常簡單。
下面是一個簡單的Fibonacci函數,我們知道它將從緩存中受益,因為它通過遞歸多次執行相同的任務。
現在我們可以使用lru_cache對其進行優化(這種優化技術稱為memoization(記憶化))。執行時間從幾秒降到幾納秒。
請查看代碼(文檔 https://www.python.org/dev/peps/pep-3132/ )。
Python 3引入了數據悄遲類,這些數據類並沒有太多限制,可以使用它們來減少樣板代碼,因為裝飾器會自動生成特殊的方法,比如__init__() 和__repr()__。根據官方建議,它們被描述為「具有默認值的可變命名元組」。
使用數據類的Armor的相同實現。
結構化Python代碼的一啟拿李種方法是在包中(帶有一個__init__.py文件的文件夾)。下面的示例是由官方Python文檔提供的。
在Python2中,上面的每個文件夾都必須有一個__init__.py文件,它會將該文件夾轉換為一個Python包。在Python3中,隨著隱式命名空間包的引入,這些文件就不再需要了。
編者注:正如一些人所說,這並不像我在本節中指出的那樣簡單,從官方的PEP 420規范來看——__init__.py對於普通包仍然是必需的,將它從文件夾結構中刪除將會把文件夾變成一個帶有附加限制的本地命名空間包,關於本機命名空間包的官方文檔對此展示了一個很好的例子,以及命名所有的限制。
就像互聯網上幾乎所有的列表一樣,本列表並不完整。我希望這篇文章至少向你展示了一個你以前並不知道的Python 3功能,它將幫助你編寫更簡潔、更直觀的代碼。一如既往,所有的代碼都可以在GitHub上找到。
(https://github.com/Weenkus/DataWhatNow-Codes/blob/master/things_you_are_probably_not_using_in_python_3_but_should/python%203%20examples.ipynb )