【ZZ】Python 學習筆記 01 – 縮進

Python捨棄了傳統的大括號,獨到的利用縮進組織代碼,使得Python代碼更整齊,更清潔,但初學Python的縮進特性,卻有着各式各樣的疑問,這些疑問層出不窮,苦惱萬分。

 

1、困惑 python

問1:若是混用Tab和空格,Python如何處理縮進的呢? 算法

(注:雖然混用空格和Tab是bug的溫牀,但這個問題讓我很困惑) 函數

 

問2:每次縮進只能用一個Tab嗎?能使用2個Tab縮進嗎?能使用多個Tab嗎? ui

(每次縮進,對於使用幾個空格、幾個Tab沒有明確規定嗎?) spa

問3:下面這個例子錯在哪裏? code

if a > 5 :
         a = 103
     print "here"

(注:第2行比第1行縮進了2個Tab,第3行比第一行縮進了1個Tab,那麼第3行還屬於if語句的block嗎?) token

 

問4:下面這個例子錯在哪裏? 接口

if a > 5 :
    a = 103
        b = 5

(注:第2行比第1行縮進了1個Tab,第3行比第1行縮進了2個Tab,那麼第3行還屬於if語句的block嗎?) get

 

2、釋惑 源碼

         (在Python內部,縮進稱爲indent,indent是向右縮進,反過來,向左縮進稱爲dedent,是用來和indent配對的)

         Python對縮進的處理分爲2個階段:詞法解析階段和語法解析階段。其中大部分處理在詞法解析階段完成。

1.         變量:tok->atbol

         Python的詞法解析源文件是parser/Tokenizer.c,其對外的接口是:PyTokenizer_Get(…)。

         PyTokenizer_Get(…)函數是對static函數tok_get(…)的簡易包裝,Python對縮進的處理的起始處就在tok_get(…)這個函數中,即文件的第1201行:

1200:    /* Get indentation level */
1201:    if (tok->atbol) {
……:        ……
1204:        tok->atbol = 0;

在進入函數tok_get(),會判斷變量tok->atbol是否爲真,「atbol」全稱是「at the begin of line」,標誌當前的詞法分析器,是否處於一行的開始。這個判斷的意圖很簡單:只有一行開始的空格和Tab才能被看成縮進處理。

tok->atbol變量是在詞法解析器每次遇到換行符’\n’時,才被置爲1。

無論怎麼樣,在1204行,每次開始處理一個新行後,tok->atbol都會被置爲0。

 

2.         混用Tab和空格會發生什麼?

      在平時的認識中,只知道1個Tab至關於4個或者8個字符的寬度,從沒想過Tab和空格混用時會發生什麼事。假設如下的討論中Tab至關於8個字符的寬度。

      Tab實際上是將當前的輸入光標,位置移到下一個8字符寬度的邊界。若是此時一行中已經輸入了7個字符,Tab只會擴展1個字符的寬度,使得輸入光標在8字符寬度的邊界。若是此時已經輸入1個字符,Tab會擴展7個字符的寬度,使得輸入光標在8字符寬度的邊界。

      那麼根據以上分析,使用nchar表明已經輸入的字符數,再輸入一個Tab後,光標的位置能夠用下面的計算公式表達:

pos = (nchar / 8 + 1) * 8

 

在Python的源碼中,有相似的代碼:

1210: col = (col/tok->tabsize + 1) * tok->tabsize;

 

其中,tok->tabsize表明一個Tab至關於幾個字符的寬度。

3.         Python實際上是將Tab轉換成空格處理縮進的

1205:for (;;) {
1206:     c = tok_nextc(tok);  
1207:     if (c == ' ')  
1208:             col++;  
1209:     else if (c == '\t')  
1210:             col = (col/tok->tabsize + 1) * tok->tabsize;  
1216:     else   
1217:             break;  1218:        
}

 

從1205行到1218行,是一個大循環,是用於計算遇到的空格、Tab數量的,一直到1217行,遇到的字符不是空格、也不是Tab時,從大循環裏break出去。

1206行的tok_nextc(tok),是讀取下一個字符的函數。

1207-1208行,若是下個字符是空格,col變量的值就加1。

1209-1210行,若是下個字符是Tab,那麼就按照將Tab擴展到tok->tabsize個字符寬度的邊界,將Tab轉換成空格的個數。

 

4.         變量tok->level,當tok->level == 0時,才進行縮進判斷

        tok->level這個變量,每次遇到小括號」(「,中括號」[「,大括號」{「時,tok->level++。

        每次遇到小括號右端」)」,中括號右端」]」,大括號右端」}」時,tok->level--。

        以此看出,tok->level並不能準確記錄小括號的嵌套數量,也不能準確記錄中括號、大括號的嵌套數量。

        其實,tok->level只是用來標識,當前是否處於括號中。詞法解析中,並不去關心括號的嵌套、配對是否正確,這些是語法解析器關心的問題。因此,在詞法解析中,只須要用這一個變量,tok->level,用來標識當前是否處於括號中就能夠了。

        以下例子:

1:  a = [ x for x in range(
2:             0, 10)  
3:      ]

 

在第2行,第3行,都有Tab縮進,但因爲是處於括號(不論是小括號、中括號、大括號)中,這些Tab都不會被Python當成縮進處理的!

故:一行開始處的Tab和空格,若是是處於括號中,是不會被當成縮進處理的!

5.         詞法解析器中的縮進棧tok->indstack

tok->indstack,是indent stack的縮寫。棧頂是由變量tok->indent指示的。

        剛開始,tok->indstack縮進棧初始化代碼以下:

k->indent = 0;
tok->indstack[0] = 0;

Python的詞法解析器,會在每次遇到縮進時,在這個縮進棧裏Push一條記錄。

        其中,棧頂是用變量tok->indent指示的,棧頂元素就是經過如下代碼讀取的:

tok->indstack[tok->indent]

 

在以前的代碼中,已經計算出了本行開始處空格的數量了,(Tab也已經轉換成了空格的數量),記錄在變量col中。

1234:        if (col == tok->indstack[tok->indent]) {
1235:                 /* No change */ 
1240:        }

下面就是開始作縮進判斷了,在1234行,若是col的值和tok->indstack棧頂元素相同時,說明本行和上一行縮進量相同,不處理。

1241:        else if (col > tok->indstack[tok->indent]) { 
1242:                 /* Indent -- always one */    …… 
1252:                 tok->pendin++;
1253:                 tok->indstack[++tok->indent] = col;
1255:        }

若是本行的col,比縮進棧tok->indstack棧頂的元素大,表示本行比上一行發生了右縮進,indent。此時,不論本行使用了多少個空格,使用了多少個Tab,通通只算做縮進了一次!即tok->pendin++。

而後,在1253行,將本行的縮進量,col,Push進縮進棧中。

       

6.         變量tok->pendin

        注意到上面的代碼中,縮進一次會tok->pendin++。

        能夠看出,這個變量tok->pendin實際上是記錄已經縮進的次數的,準確的來講,若是tok->pendin == 0,說明沒有發生過縮進。

        若是tok->pendin > 0,說明發生個tok->pendin次右縮進,即indent。

        若是tok->pendin < 0,說明發生過ABS(tok->pendin)次左縮進,即dedent。

        在Python的語法中,indent和dedent的個數必須配對,發生過幾回indent,就要發生幾回dedent,否則就會有語法錯誤。

        tok->pendin這個變量,並不在詞法分析時起做用,而是語法解析器調用詞法解析器的PyTokenizer_Get(…)接口時,發生做用。

 

7.         發生了dedent

        下面就是col比tok->indstack棧頂元素小的狀況,即發生了dedent:

1256:        else {  1258:                 while (tok->indent > 0 && 
1259:                           col < tok->indstack[tok->indent]) { 
1260:                           tok->pendin--; 
1261:                           tok->indent--;  1262:                 } 
1263:                 if (col != tok->indstack[tok->indent]) { 
1264:                           tok->done = E_DEDENT; 
1265:                           tok->cur = tok->inp; 
1266:                           return ERRORTOKEN; 
1267:                 }  1272         }

 

首先,在1258-1262行,將縮進棧中比col大的元素Pop出來,Pop多少元素,就表明發生了幾回dedent,經過tok->pendin--,來記錄發生了幾回dedent。

        在1263行,Pop出比col大的元素後,此時的col必須和縮進棧頂部的元素必須相同,不然就會報錯!這裏能夠解釋本文最開始提出的問題:

 

1.      if a > 5 :  
2.                       a = 10  
3.               print 「here」

剛開始,tok->indstack縮進棧初始化時只有1項元素0,能夠記爲 [ 0 ];

當詞法解析器處理第1行時,沒有任何空格和Tab,col的值爲0,和tok->indstack的棧頂元素相同,說明沒有發生任何縮進。

詞法解析器處理第2行時,有2個Tab,按照以前Tab轉換成空格的算法,col的值爲16,那麼發生一次indent,並將當前縮進量Push進tok->indstack內,即當前的tok->indstack能夠記爲[ 0, 16 ];

在第3行,有1個Tab,按照以前的算法,col的值爲8,比tok->indstack棧頂元素要小,那麼就將比col大的元素都Pop出來,此時tok->indstack能夠表示爲 [ 0 ];

 可是此時tok->indstack棧頂的元素0,和col的值8不相同,Python詞法解析器就會報錯!

 

總結:每次縮進indent,Python並不要求使用多少個空格和Tab,只要比上一行的縮進量大就算一次縮進;每次dedent,卻須要和以前某次indent的量匹配!

 

8.         語法中的縮進處理

        Python的縮進分在詞法分析器和語法分析器2個地方處理,上面所述的都是詞法分析器的內容。下面是涉及到語法分析器的地方。

        Python的語法分析器的代碼能夠用如下僞碼來描述:

1:      while(true) {  
2:               token = PyTokenizer_Get();  
3:               do_something( token );  
4:      }

 

語法解析器每次從詞法解析器中獲取一個詞,Token。(經過PyTokenizer_Get調用)。

每獲取一個Token,語法分析器就往前分析一步,一直到全部Token都處理完成。

Python全部的語法規則,都在Grammar/Grammar這個文件中,其中涉及到indent和dedent的,只有一處:

suite:simple_stmt | NEWLINE INDENT stmt+ DEDENT

   而suite,叫作block也許更好理解些,用在:if,for,while,def,class,try-except-finally這些語句中,簡易列出以下:

if test : suite
for exprlist in testlist : suite
while test : suite
def Name() : suite
class Name : suite
try : suite 
except : suite
finally : suite

這裏,只須要注意:INDENT和DEDENT必須是配對出現的!而且只能出如今以上這些語句中!

那麼,以前的這個例子出錯的緣由就能夠解釋了:

1.      if a > 5 :  
2.               a = 10  
3.                        b = 5

 

在第2行,由於是if語句的剛開始處,因此此時的縮進是合法的。

可是在第3行,並無語法規定此時能夠縮進,因此此處的縮進是不合法的!語法分析器會報錯!

 

9.         tok->pendin

        此時再來看語法解析器,僞碼以下:

1:      while(true) {  
2:               token = PyTokenizer_Get();  
3:               do_something( token );  
4:      }

對於下面這個例子:

1:      if a > 10:  
2:               if b > 10:  
3:                        a = 5;  
4:      print 「here」

 

語法解析器每次調用PyTokenizer_Get(),獲取下一個Token,

在第2行,發生了1次縮進,PyTokenizer_Get()會返回一個INDENT,

 在第3行,比第2行又縮進了1次,PyTokenizer_Get()又會返回一個INDENT,

到了第4行,須要2個DEDENT才能和以前的2個INDENT配對!也就是說,語法解析器調用PyTokenizer_Get(),連續2次調用都返回DEDENT才行。

 

        繼續看Parser/Tokenizer.c中的tok_get(…)函數代碼:

1279:     if (tok->pendin != 0) {  
1280:              if (tok->pendin < 0) {  
1281:                       tok->pendin++;  
1282:                       return DEDENT;  
1283:              }  
1284:              else {  
1285:                       tok->pendin--;  
1286:                       return INDENT;  
1287:              }  
1288:     }

 

通過以前的代碼,tok->pendin若是爲0,說明本行沒有發生過縮進,tok_get(…)就不會返回INDENT或者DEDENT;

1280行,若是tok->pendin < 0,說明發生過左縮進,即dedent,那麼tok_get(…)就返回DEDENT,而且tok->pendin++;

1284行,若是tok->pendin > 0,說明發生過右縮進,即indent,那麼tok_get(…)就返回INDENT,而且tok->pendin--;

 

再看以前的例子:

1:      if a > 10:  
2:               if b > 10:  
3:                        a = 5;  
4:      print 「here」

 

 

在第2行,發生了一次indent,tok->pendin的值爲1,此時tok->pendin > 0,就會執行tok->pendin--,並返回INDENT,tok->pendin的值恢復爲0;

在第3行,又發生了一次indent,tok->pendin的值爲1,此時tok->pendin > 0,又會tok->pendin--,並返回INDENT,tok->pendin的值恢復爲0;

在第4行,發生了2次dedent,tok->pendin的值爲-2,此時tok->pendin < 0,就會tok->pendin++,並返回DEDENT,tok->pendin的值爲-1

語法解析器再次調用PyTokenizer_Get(…)時,此時tok->pendin == -1,(而且此時tok->atbol != 0),判斷tok->pendin < 0,又會tok->pendin++,並返回DEDENT,此時的tok->pendin的值才恢復成0。

 

 這就是tok->pendin的做用。

 

 到此,關於Python縮進的分析就完成了。

相關文章
相關標籤/搜索