【ZZ】Python 學習筆記 02 – List 推導式

 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 }
 
語法解析是會報錯的!由於在Dictionary(或Set)的推導式語法規則中,for…in…中,不能是全部元素的列表!

爲何會有這種不一樣的語法呢?我還沒搞明白!

另外,還會發現testlist_old,裏面還一個」old」單詞,這個很容易引發頭疼,由於加了」old」這個單詞,頗有多是爲了和老版本兼容而出現的語法規則。而歷史遺留問題,是最讓人頭疼的問題了。

在Python的語法規則裏面,還有一個叫作testlist的語法規則,那麼這個testlist_old中的」old」究竟是什麼意思呢?

通過幾番考究,原來以下,一個簡單的例子:

a = 5 if b > 3 else 2
上面是Python中相似C的三元運算符」?:」的語法,在Python內部,稱爲」if-expr」。

這個語法,就是沒有」old」的語法。而在testlist_old語法中,這種帶有if-else的語法是不容許的。

爲何呢?例如,在list comprehension中,能夠寫成這樣:

a = [ x for x in 5 if b > 3 else 2, 3 ]
在這個例子中,if-else本意上是: 5 if b > 3 else 2,組成一個上面所說的」if-expr」語句的,可是卻和List Comprehension中的for、if語法發生了衝突:這裏這個if究竟是List Comprehension中的呢?仍是if-expr中的呢?

    爲了杜絕這種歧義,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語句
也就是說,進入每個for語句後,先取出Object的迭代器(經過GET_ITER),而後不斷對其執行FOR_ITER指令,每次迭代出一個元素,都要對從屬的if語句進行判斷,若是有一個爲False,至關於continue,返回FOR_ITER處,迭代下一個元素。

    若是全部的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原來的做用域是什麼,如今仍是什麼。

相關文章
相關標籤/搜索