Python語言以簡易明瞭著稱,但初次學習Python,卻被不少語法搞的昏頭漲腦。List comprehension絕對是其中之一。 python
1、困惑 正則表達式
問題一:列表推導式中的變量,是全局變量?仍是局部變量?仍是閉包變量? 數組
注:一個簡單的列表推導式,以下: 閉包
a = [ x for x in range(10) ]
這裏的變量x,是局部變量嗎?在列表推導式結束後,還能訪問變量x嗎? app
問題二:列表推導式,推導過程究竟是從左往右?仍是從右往左? 函數
注:一個簡單的列表推導式,以下: 學習
a = [ (x, y) for x in range(2) for y in range(3) ]
若是寫成僞碼,是: spa
for x in range(10): for y in range(10: a.append((x, y))
仍是: code
for y in range(10): for x in range(10): a.append((x, y))
雖然這個問題看起來很奇葩,但讓我很困惑。 對象
問題三:列表推導式中,for語句和if語句之間的關係是什麼呢?
注:不光是for語句和if語句之間的關係,多個for語句之間,多個if語句之間,for語句和if語句之間,它們的關係又是什麼樣的呢?
問題四:列表推導式,究竟是怎麼運行的呢?
注:雖然看到列表推導式,憑藉猜想,也能猜個八九不離十。但總不知道List推導式具體如何運行的,心中總不踏實。
2、釋惑
1. 關於GET_ITER、FOR_ITER字節碼(opcode)
Python中有迭代的概念,最經常使用的要數for循環了。一個簡單的for循環以下:
1: for x in A: 2: do_something 3:
對於這個for循環,Python會大體生成以下字節碼:
1: iter = GET_ITER( A ) 2: x = FOR_ITER(iter) 3: if not x : jump to 6 4: do_something… 5: jump to 2 6: (The End)
在這裏面,起到重要做用的,就是GET_ITER、FOR_ITER這2個字節碼。
其中,GET_ITER是取出一個Object的迭代器(調用PyObject_GetIter(),若是是一個類的對象,調用其__iter__方法);
以後,就會不斷對這個迭代器執行FOR_ITER指令(若是是一個類的對象,調用其next方法);
若是FOR_ITER指令迭代不到下一項了(一般是遇到StopIteration異常了),就跳出for循環,不然會一直迭代下去。
2. 關於POP_JUMP_IF_FALSE指令
這個指令比較長,第一次見有些被嚇到,不知是幹什麼的。
可是仔細一看,這個指令仍是比較好理解的:
首先,Python的每一個函數都有本身的運行棧,全部的臨時運算結果都要放在這個棧上的,例如如下簡單代碼:
1: if a > 0: 2: do_something這裏的變量a,若是是個全局變量,那麼它存在全局變量Dictionary裏;若是它是個局部變量,那麼它存放在局部變量數組裏;若是它是個閉包變量,那麼它存放在閉包變量數組裏,總之,它是不在函數運行棧裏的。
可是,變量a和常量0的邏輯比較的結果,是一個臨時的運算結果,這個運算結果是要放在函數運行棧裏的,以方便後面if判斷時使用。
那麼,Python對這麼個簡單的if代碼,會大體生成如下字節碼:
1: 進行a > 0判斷, 2: Push(結果) 3: POP_JUMP_IF_FALSE 4: do_something… 5: (The end)
Python會在邏輯運算後,將邏輯運算的結果自動Push進函數的運行棧內。那麼,在執行指令POP_JUMP_IF_FALSE時,會進行下面的操做:
// POP_JUMP_IF_FALSE 一、x = POP() 二、if not x : jump
POP_JUMP_IF_FALSE指令,其實就是將棧頂的元素(通常是剛進行邏輯運算的結果)Pop出來,而後判斷其是否爲False,若是是False,那麼就跳轉,不然什麼事也不作。
3. List comprehension的語法
在剛看到List Comprehension時,很不能理解這個語法,總會有一個疑問:在List Comprehesion中,是否只能寫for和if語句?可否寫while語句?可否寫try-except-finally語句?並且for語句和if語句之間的關係是什麼?
有不少疑問,最終還得看Grammar/Grammer這個文件中,定義的語法規則。
其中,List comprehension的規則,在Grammar文件中,稱爲listmaker。
listmaker分爲2種,最簡單的一種,以下:
a = [1, 2, 3, 4, 5]
也就是直接列出List中的全部元素。這種方式最簡單,也最好理解。
第二種就是本文所說的List Comprehension了,語法以下:
一、listmaker: test list_for 二、list_for: for’ explist ‘in’ testlist_safe [list_iter] 三、list_iter: list_for | list_if 四、list_if: if’ old_test [list_iter]
語法文件全是正則表達式,並且先後相互引用,讀起來很是吃力。不過在上面所列的這4行語法規則中,能夠看到:list comprehension中,只能使用for和if這2種語句。這也解決了一大部分疑問。
並且能夠從上面的語法中看出,每一個for語句後面,還能夠接一個for語句或者一個if語句;每一個if語句後面,也能夠接一個for語句或者一個if語句;而且沒有對for語句、if語句的個數有任何限制。
若是注意看上面關於list_for語法的規則,能夠發現裏面有一個叫testlist_safe的東西,這裏要和Dictionary的推導語法規則對照一下,會發現頗有趣的現象。
Dictionary推導式的一部分語法規則以下:
comp_for: 'for' exprlist 'in' or_test [ comp_for ]
這裏的comp_for語法規則,幾乎和上面的list_for語法規則相同,惟一不一樣的是在list_for語法規則中的testlist_safe位置上,變成了or_test。
只從字面看來,testlist_safe和or_test相比,中間有一個’list’單詞,也就是說:testlist_safe能夠是一個列表,而or_test不能夠,舉例以下:
a = [ x for x in 1, 2, 3, 4 ]
在構造列表a時,能夠直接在for … in …中,列出全部的元素,即上面的「1, 2, 3, 4」;可是,若是是在構造一個Dictionary(或Set),以下:
a = { x for x in 1, 2, 3, 4 }
爲何會有這種不一樣的語法呢?我還沒搞明白!
另外,還會發現testlist_old,裏面還一個」old」單詞,這個很容易引發頭疼,由於加了」old」這個單詞,頗有多是爲了和老版本兼容而出現的語法規則。而歷史遺留問題,是最讓人頭疼的問題了。
在Python的語法規則裏面,還有一個叫作testlist的語法規則,那麼這個testlist_old中的」old」究竟是什麼意思呢?
通過幾番考究,原來以下,一個簡單的例子:
a = 5 if b > 3 else 2
這個語法,就是沒有」old」的語法。而在testlist_old語法中,這種帶有if-else的語法是不容許的。
爲何呢?例如,在list comprehension中,能夠寫成這樣:
a = [ x for x in 5 if b > 3 else 2, 3 ]
爲了杜絕這種歧義,Python在語法規則中,就使用testlist_old而不是testlist,使得if-expr語句不能出如今for…in…的元素列表中。
4. List Comprehension中,for語句和if語句是什麼關係呢?
Python的List Comprehension中,可使用無限多個for、if語句,該怎麼去理解這些for、if語句呢?它們之間的關係是什麼呢?
Python的語法解析、字節碼生成,大約分爲3個階段:
一、將.py源代碼,解析成語法樹
二、將語法樹,解析成AST樹
三、根據AST樹,生成字節碼
對於List Comprehension,能夠在第2階段,即Python/Ast.c這個源文件中,發現for語句和if語句之間的關係。
Python在從語法樹生成AST的過程當中,會將List Comprehension中的for分離開來,並將每一個if語句,所有歸屬於離他最近的左邊的for語句,例如:
a = [ (x, y) for x in range(10) if x % 2 if x > 3 for y in range(10) if y > 7 if y != 8 ]
上面這段代碼中,有2個for和4個if,分別以下:
一、for x in range(10)
二、for y in range(10)
三、if x % 2
四、if x > 3
五、if y > 7
六、if y != 8
在AST的過程當中,Python會按照for語句將上面的語句拆成2部分,分別以下:
每一個if語句,從屬於離他最近的左邊的for語句。
下面看語法解析的第三階段,即:經過AST生成字節碼,在源代碼Python/Compiler.c文件中。
在Python/Compiler.c源文件中,處理List Comprehension的代碼,主要是2592行的compiler_listcomp_generator(…)函數。
這個函數,首先會生成字節碼:BUILD_LIST,即生成一個新的List;而後經過自身的遞歸,從左到右,依次處理AST過程生成的for語句及其從屬的if語句。
其中對於每個for語句,大抵生成如下字節碼:
1: GET_ITER 2: FOR_ITER
而後從左到右,依次處理從屬與這個for語句的if語句,大抵生成如下字節碼:
1: 進行邏輯判斷並將結果Push進函數棧 2: POP_JUMP_IF_FALSE(若是結果爲False,則跳轉到XX處) XX: 跳轉到for語句的FOR_ITER處執行,至關於continue語句
若是全部的if都判斷爲True,才進入後續的for語句中執行(後續的for語句都嵌套在以前的for語句中),直到最後一個for語句執行結束(從屬的if都判斷爲True),這時才向list中append一個新的元素。
整個過程的僞碼能夠以下:
for xx in xxx: if not xxxx: continue; if not xxxxx: continue; ........ for xx in xxx: if not xxxx: continue; ........ // 到了最後一個for for xx in xxx: ....... List.append(value)
至此,List Comprehension的內部運行過程就搞明白了。
5. 最後一個問題,列表推導式中的變量,是局部變量嗎?
例如,一個簡單的例子:
a = [ x for x in range(10) ]
這裏面的變量x,是局部變量嗎?在列表推導式結束後,還能夠訪問變量x嗎?
Python的變量做用域,是在源代碼Python/Symtable.c中實現的。
關於Python的變量做用域,打算再寫一篇另外的文章介紹。
這裏的結果是:若是變量x沒有被使用過,那麼變量x會成爲一個局部變量,當列表推導式結束後,還能夠訪問變量x;不然,變量x原來的做用域是什麼,如今仍是什麼。