【ZZ】Python 学习笔记 01 – 缩进

本文详细解析Python缩进的处理机制,包括混用Tab和空格的处理方式,Tab转换为空格的过程,Python如何计算缩进,以及缩进栈的使用。文章进一步解释了Python语法中的缩进规则,特别是如何处理if语句、for循环等块级语句中的缩进问题。通过具体的代码实例,清晰地展示了Python在词法解析和语法解析阶段如何处理缩进,帮助开发者更好地理解和应用Python的缩进特性。
Python舍弃了传统的大括号,独到的利用缩进组织代码,使得Python代码更整齐,更清洁,但初学Python的缩进特性,却有着各式各样的疑问,这些疑问层出不穷,苦恼万分。

 

一、困惑

问1:如果混用Tab和空格,Python如何处理缩进的呢?

(注:虽然混用空格和Tab是bug的温床,但这个问题让我很困惑)

 

问2:每次缩进只能用一个Tab吗?能使用2个Tab缩进吗?能使用多个Tab吗?

(每次缩进,对于使用几个空格、几个Tab没有明确规定吗?)

问3:下面这个例子错在哪里?

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

(注:第2行比第1行缩进了2个Tab,第3行比第一行缩进了1个Tab,那么第3行还属于if语句的block吗?)

 

问4:下面这个例子错在哪里?

if a > 5 :
    a = 103
        b = 5

(注:第2行比第1行缩进了1个Tab,第3行比第1行缩进了2个Tab,那么第3行还属于if语句的block吗?)

 

二、释惑

         (在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缩进的分析就完成了。

 

 

 

转载于:https://my.oschina.net/u/1178546/blog/160618

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值