Python 中列表推導(list comprehension)相對於循環有什麼優勢?性能會更高嗎?

python中的列表推導(list comprehension)一般用於從一個列表計算出另一個列表,從功能上看是map/filter的結合體,也能通過循環實現。之前查過的一些相關的資料,有人說列表推導只是語法糖,也有說列表推導比循環和map/filter的寫法效率更高(只給了一個測試結果,沒有相關分析),其他有價值的資料就沒有找到了...這是某次一個面試官問的問題,我想還是要搞清楚吧,所以就來知乎請教各位大神了。

python的設計哲學裡,有一句「There should be one-- and preferably only one --obvious way to do it.」,那麼如果列表推導和循環以及map/filter實現上沒有什麼區別的話,應該就不會存在了吧?當然python的哲學裡還有「Beautiful is better than ugly.」,貌似也有可能只是為了好看的樣子...


首先肯定 map 和列表推導效率確實會比循環的高,

先說列表推導,下邊是我在 ipython 里的測試結果(測試環境 Python 2.7.10):

&>&>&> long_list = range(1000)

&>&>&> a = []

&>&>&> %timeit for i in long_list: a.append(i+1)
10000 loops, best of 3: 100 μs per loop

&>&>&> %timeit [i+1 for i in long_list]
10000 loops, best of 3: 43.3 μs per loop

可以看出列表推導還是要快過 for 循環的。

那為什麼列表推導會快呢?我們直接調用 python 的 dis 模塊去看看他的位元組碼:

這個是列表推導那一行代碼的位元組碼:

0 BUILD_LIST 0
3 LOAD_GLOBAL 0 (long_list)
6 GET_ITER
&>&> 7 FOR_ITER 16 (to 26)
10 STORE_FAST 0 (i)
13 LOAD_FAST 0 (i)
16 LOAD_CONST 1 (1)
19 BINARY_ADD
20 LIST_APPEND 2
23 JUMP_ABSOLUTE 7
...

這個是 for 循環那一行的位元組碼:

6 SETUP_LOOP 31 (to 40)
9 LOAD_GLOBAL 0 (long_list)
12 GET_ITER
&>&> 13 FOR_ITER 23 (to 39)
16 STORE_FAST 1 (i)
19 LOAD_FAST 0 (a)
22 LOAD_ATTR 1 (append)
25 LOAD_FAST 1 (i)
28 LOAD_CONST 1 (1)
31 BINARY_ADD
32 CALL_FUNCTION 1
35 POP_TOP
36 JUMP_ABSOLUTE 13
...

對比一下不難發現其實列表推導和 for 循環的過程幾乎是一樣的,除了如何append。所以你要說他是語法糖也不是不行……

在列表推導中直接使用了『LIST_APPEND』這個位元組碼來實現 append 功能,效率相當的高。而在 for 循環中每次循環都要先載入 append 這個屬性然後再 『CALL_FUNCTION』一下。這樣勢必就會慢了很多。為了驗證我們的猜想,我們把 append 這個函數存到局部變數里去:

&>&>&> a = []

&>&>&> invoke = a.append

&>&>&> %timeit for i in long_list: invoke(i+1)
10000 loops, best of 3: 67.2 μs per loop

發現沒有比前一個版本的 for 循環快了接近40%,剩下的多出來20多 μs 的開銷自然就是『CALL_FUNCTION』的開銷咯 ╮(╯_╰)╭。

相信到這裡你應該明白了為什麼列表推導要比 for 循環快吧,秘訣就在這個『LIST_APPEND』這個位元組碼上,相當於你直接調用了 C 語言版本的函數(不嚴謹)而且越過了一些中間步驟。

接下來簡單說說 map 的事情,直接使用 map 一般來說是要比循環快的,但有的時候情況會比較詭異,例如:

&>&>&> %timeit for i in long_list: a.append(i+1)
10000 loops, best of 3: 100 μs per loop

&>&>&> %timeit map(lambda x: x+1, long_list)
10000 loops, best of 3: 109 μs per loop

坑爹呢(╯‵□′)╯︵┻━┻,反倒還慢了好不好!

別急,我們把 map 的寫法改成這樣:

&>&>&> int_object = 1

&>&>&> %timeit map(int_object.__add__, long_list)
10000 loops, best of 3: 41.6 μs per loop

於是神奇的事情出現了!基本上和列表推導一樣快!(⊙o⊙)

這個主要是因為 lambda 表達式生成的函數是 Python 的,而直接用+運算符或者__add__方法調用的是 C 版本的。你要是把列表推導裡邊的+換成 lambda 表達式兩者的速度差不多,map 一般來說還要快上一點點。本質上來說 map 調用了底層的 C 函數所以速度自然是快的。粗暴的總結一下就是不用 lambda 的時候 map 要快一些喲~~

OK,差不多就是醬紫~

----補充-----

剛在 Python3 裡邊試了試貌似 Python3 裡邊的 map 改成惰性的了,直接返回一個 map 對象所以要用%timeit list(map(lambda x: x+1, long_list))才能得到類似的結果。


「There should be one-- and preferably only one --obvious way to do it.」

列表推導就是那個obvious way,你不應該使用的是map和filter。有這兩個只是因為設計出這個語法之前就實現了那倆。它們的確比循環快一點,但最重要的還是表意很明確。


效率高一點,去看 dis 模塊。

Python 不喜歡 FP 的,而且 map / filter 是惰性的,list comprehension 不是。

循環比 list comprehension 更底層而難以被理解一些。比較:「把這個列表裡的所有元素乘以二」和「創建一個空列表 B,對列表 A 里的每一個元素,將其乘以二的結果添加到 B 的尾部」。

PS: 對於每一個可以用多種途徑實現的用例,我基本上都可以確定一個最優的途徑。


列表推導是在C環境下運行的,所以非常快,循環的話也要看循環體涉及的數據結構和循環本身複雜程度,例如用pandas的DataFrame索引又要遍歷循環的話相信我會非常慢。

回歸到具體的使用場景,取捨要考慮多方因素,比如實現問題的邏輯複雜程度,後續的維護要求,以及潔癖強迫症這種事...


推薦閱讀:

如何在visual studio上寫 python?
如何在用 Python 編程時添加中文注釋?
零基礎小白學編程多久能達到接私活的水平?
python pandas 怎樣高效地添加一行數據?
參加數學建模有沒有必要學python?

TAG:編程語言 | Python | Python開發 |