天天看點

Python 疑難問題:[] 與 list() 哪個快?為什麼快?快多少呢?

本文出自“Python為什麼”系列,請檢視全部文章

在日常使用 Python 時,我們經常需要建立一個清單,相信大家都很熟練了吧?

上面的兩種寫法,你經常使用哪一個呢?是否思考過它們的差別呢?

讓我們開門見山,直接抛出本文的問題吧:兩種建立清單的 [] 與 list() 寫法,哪一個更快呢,為什麼它會更快呢?

注:為了簡化問題,我們以建立空清單為例進行分析。關于清單的更多介紹與用法說明,可以檢視這篇文章

對于第一個問題,使用<code>timeit</code>子產品的 timeit() 函數就能簡單地測算出來:

如上圖所示,在各自調用一千萬次的情況下,[] 建立方式隻花費了 0.47 秒,而 list() 建立方式要花費 1.75 秒,是以,後者的耗時是前者的 3.7 倍!

這就回答了剛才的問題:建立空清單時,[] 要比 list() 快不少。

注:timeit() 函數的效率跟運作環境相關,每次執行結果會有微小差異。我在 Python3.8 版本實驗了幾次,總體上 [] 速度是 list() 的 3 倍多一點。

那麼,我們繼續來分析一下第二個問題:為什麼 [] 會更快呢?

這一次我們可以使用<code>dis</code>子產品的 dis() 函數,看看兩者執行的位元組碼有何差别:

如上圖所示,[] 的位元組碼有兩條指令(BUILD_LIST 與 RETURN_VALUE),而 list() 的位元組碼有三條指令(LOAD_NAME、CALL_FUNCTION 與 RETURN_VALUE)。

這些指令意味着什麼呢?該如何了解它們呢?

首先,對于 [],它是 Python 中的一組字面量(literal),像數字之類的字面量一樣,表示确切的固定值。

也就是說,Python 在解析到它時,就知道它要表示一個清單,是以會直接調用解釋器中建構清單的方法(對應 BUILD_LIST ),來建立清單,是以是一步到位。

而對于 list(),“list”隻是一個普通的名稱,并不是字面量,也就是說解釋器一開始并不認識它。

是以,解釋器的第一步是要找到這個名稱(對應 LOAD_NAME)。它會按照一定的順序,在各個作用域中逐一查找(局部作用域--全局作用域--内置作用域),直到找到為止,找不到則會抛出<code>NameError</code>。

解釋器看到“list”之後是一對圓括号,是以第二步是把這個名稱當作可調用對象來調用,即把它當成一個函數進行調用(對應 CALL_FUNCTION)。

是以,list() 在建立清單時,需要經過名稱查找與函數調用兩個步驟,才能真正開始建立清單(注:CALL_FUNCTION 在底層還會有一些函數調用過程,才能走到跟 BUILD_LIST 相通的邏輯,此處我們忽略不計)。

至此,我們就可以回答前面的問題了:因為 list() 涉及的執行步驟更多,是以它比 [] 要慢一些。

看完前兩個問題的解答過程,你也許覺得還不夠過瘾,而且可能覺得就算知道了這個冷知識,也不會有多大的幫助,似乎那微弱的提升顯得微不足道。

但是,我們<code>Python貓</code>出品的《Python為什麼》系列一直秉承着孜孜不倦的求知精神,是不可能放着這個問題不去回答的。

而且,由于有發散性思考的習慣,我還想到了另外一個挺有意思的問題:list() 的速度能否提升呢?

我不久前寫過一篇文章 正好讨論到這個問題,也就是在剛剛釋出的 Python 3.9.0 版本中,它給 list() 實作了更快的 vectorcall 協定,是以執行速度會有一定的提升。

感興趣的同學可以去 Python 官網下載下傳 3.9 版本。

根據我多輪的測試結果,在新版本中運作 list() 一千萬次,耗時大概在 1 秒左右,也就是 [] 運作耗時的 2 倍,相比于前面接近 4 倍的資料,目前版本總體上是提升了不少。

至此,我們已回答完一連串的疑問,如果你覺得有收獲,請點贊支援!歡迎大家關注後續更多精彩内容。

本文屬于“Python為什麼”系列(Python貓出品),該系列主要關注 Python 的文法、設計和發展等話題,以一個個“為什麼”式的問題為切入點,試着展現 Python 的迷人魅力。所有文章将會歸檔在 Github 上,歡迎大家給顆小星星,項目位址:https://github.com/chinesehuazhou/python-whydo