LiouPerng

导航

 

  最近开始学习Python语言,首先引起我注意的是它比较独特的语法。

  一是在行首缩进(Indentation)的使用上,Python与其他大多数语言不同:在大多数语言中,缩进仅是为了提升代码的可读性,增删缩进并不影响程序的最终运行结果;而在Python语言中,缩进有着重要的语法意义,对缩进运用不当,会引发语法或逻辑错误。在我所知道的编程语言中,仅ABC和Haskell采用了与Python相似的缩进语法(Python之父Guido van Rossum曾在ABC语言项目组中工作过)。

  二是在书写风格上,许多编程语言采用自由格式(Free-format),并通常以分号作为语句间的分隔符号,使得同一条语句可以跨越多行,多条语句也可以写在同一行。而在大量的Python示例程序中,通常是一条语句只占一行,一行只写一条语句。

  以下边两段C++代码为例,第2-14行的程序,与第16-21行的程序是等价的(虽然后者风格极差)。

 1 /*C++*/
 2 #include <iostream>
 3 using namespace std;
 4 int main(){
 5     int i,j;
 6     for(i=1;i<=9;i++){
 7         for(j=1;j<=i;j++){
 8             cout<<j<<'*'<<i<<'='<<i*j;
 9             cout<<'\t';
10         }
11         cout<<endl;
12     }
13     return 0;
14 }
15 
16 #include <iostream>
17 using namespace std;  int main(){int i,j;
18     for(i=1;i<=9;i++){for(j=1;j<=i;j++){
19   cout<<j<<'*'<<i<<'='
20       <<i*j;
21 cout<<'\t';}cout<<endl;}return 0;}

但是下边这段Python代码,缩进并不能随意增删。例如:若将第3行删除一个缩进,系统会报"IndentationError: expected an indented block";若将第5行增加一个缩进,则程序无语法错误,但运行结果将发生变化。

1 for i in range(1,10):
2     for j in range(1,i+1):
3         print j,'*',i,'=',i*j,
4         print '\t',
5     print

  之所以在Python源代码中不能像C++等其他语言那样随意增删缩进,是因为:在其他编程语言中,通常有特定的关键字或符号来专门负责标识语句块(Blocks)的开始和结束(如ALGOL系语言的 begin...end ,SAS语言的 do...end ,C类语言的 {...} ,LISP系语言的 (...) 等等),缩进则专门用来增加代码的可读性或层次感,两者各司其职;而Python只用":"来标识块的开始,却没有用来标识块结束的关键字或符号,要依赖“块中内容增缩进,块结束时删缩进”,缩进自己同时肩负了明确标识和方便阅读两项职责。

  

再就是由于每行只能有行首一个缩进,而不同行之间可能拥有各自不同的缩进级别,自然不宜轻易拆分或合并。但是简洁的高目标是有代价的,至少在我练习写代码的过程中,碰到了比其他语言更多的困惑,主要有以下几点:

  1. 在缩进中混合使用tab与空格,常常会引起报错,怎样才能不出错?
  2. 不同位置同一级别的缩进,似乎可以开始于不同的列,但是一搞不好就会引起报错,到底是怎样的规则?
  3. 多条语句其实可以写在同一行之上,之间需要以分号分隔,但是复合语句却不能写在同一行,为什么?
  4. Python行结束语句即结束,如果语句太长,写在一行不易读,如何拆分成多行?

对以上几点问题,我们可以简单规避:

  1. 在缩进中仅使用tab,或仅使用空格,绝不同时使用。
  2. 仅在相同位置开始书写不同缩进级别的代码,如第1级顶格,第2级空4个空格或1个tab,第3级空8个空格或2个tab…
  3. 不管什么语句,永远不使用分号在同一行并列,一行只写一句。
  4. 不管多长,一句只写在一行,易读与否不考虑。

作为一名有理想,有抱负,特别是有钻研精神的程序员,我无法满足于以上鸵鸟式回答。经过多方查资料,认为比较靠谱的,一是德国哥们儿在其网站上对相关内容做的FAQ(http://www.secnetix.de/olli/Python/block_indentation.hawk),二是语言参考手册与英文版维基百科。经过学习与验证,已能回答前述4个困惑,顺便还了解了许多一般教程上没有的内容,现将相关内容整理总结如下:

A Python program is read by a parser. Input to the parser is a stream of tokens, generated by the lexical analyzer. This chapter describes how the lexical analyzer breaks a file into tokens.

  • 行结构与行连接

  (主要参考https://docs.python.org/2/reference/lexical_analysis.html#line-structure

  首先,Python会将源代码文本组织为逻辑行(Logical Lines),逻辑行尾会被Python加上NEWLINE标记。我们知道,文本文件采用回车(Mac)、换行(Unix/Linux),或回车换行(DOS/Windows)作为行结束的标志,在Python中,这样形成的行叫作物理行(Physical Lines)。一般情况下,一个物理行即是一个逻辑行,但是通过显式或隐式的行连接规则,我们可以在Python源代码中,将多个物理行连接到一起而创建一个逻辑行。

  所谓显式行连接,是指可通过在物理行的结尾,采用反斜杠"\"来连接多个物理行形成逻辑行,如:

1 if 1900 < year < 2100 and 1 <= month <= 12 \
2    and 1 <= day <= 31 and 0 <= hour < 24 \
3    and 0 <= minute < 60 and 0 <= second < 60:   # Looks like a valid date
4         return 1

其中的第2-3行,通过显式连接,与第1行连接成一行,并在行尾加入NEWLINE标记。随后第4行尾也加入NEWLINE标记,4个物理行变为2个逻辑行,第2逻辑行为if语句条件成立时的执行部分。

  所谓隐式行连接,是指表达式中的圆括号(())、方括号([])、花括号({})可被分写在多行,而不用反斜杠来连接,如:

1 month_names = ['Januari', 'Februari', 'Maart',      # These are the
2                'April',   'Mei',      'Juni',       # Dutch names
3                'Juli',    'Augustus', 'September',  # for the months
4                'Oktober', 'November', 'December']   # of the year

全部4行,因在一对匹配的中括号中,被隐式连接成一行,并在行尾加入NEWLINE标记,4个物理行变为1个逻辑行。

  需要注意的是,显式连接或隐式连接,在与注释或字符串字面量交织的时候,有不同的限制,具体参见语言参考手册。

  在了解行连接后,问题4得以解决,我们可以通过行连接,来把过长的语句写在多行。

  • 缩进处理 

  (主要参考https://docs.python.org/2/reference/lexical_analysis.html#indentation

  1. 逻辑行开始处的引导空白符(空格与tab)用于计算行缩进级别,然后再用缩进级别决定语句的分组。注意,仅由tab、空格、换行或注释组成的逻辑行,将不参与缩进级别计算。
  2. 首先,tab被从左到右替换为1至8个空格,以使包含替换部分的字符总数正好为8的倍数(这是为了和Unix 使用一样的规则)。之后,第1个非空白字符前的空格总数用以确定行的缩进。也就是说,像     if a>5:    print 'a>5';    print True 中,if后面的空格与tab,不参与确定缩进级别。
  3. 如有通过反斜杠进行显式行连接,则只是第1个反斜杠前的部分用于确定缩进。这不难理解,因为缩进担负着视觉上分辨层次感的需求,如果缩进可以出现在多个物理行,将十分难以观察运用。
  4. 换页符(Formfeed,缩写为FF)如出现在行首,则将被上述缩进计算过程忽略。换页符如出现在其他位置并在引导空白符中间,则效果是未定义的(如它有可能重置空格计数为0)。

  以上缩进处理过程1-3能够解释问题1中为何混用易错:确实搞不好很容易出错。tab与空格不同,空格虽然只占位而无“身影”,但在使用等宽字体的情况下,它至少能保证每个空格所占位置与可显示的ASCII字符是一样的,而tab的效果则显得飘忽不定。如以s表示空格,t表示tab,我们来看几个计算题:

ssss:1+1+1+1=4个空格
tttt:8+8+8+8=32个空格
stsstssstsssst:1+7+1+1+6+1+1+1+5+1+1+1+1+4=32个空格

看起来也不算很复杂是吗?但是更麻烦的问题在于:Python固定将tab试着从1到8做补到8的整倍数处理,而在其他软件(如IDE或VIM)那里,可能也做类似的处理,但却可能将8替换为其他数字(如2,4,6,12,16等等)来进行显示,更有甚者还有可能在这个基础上将按键tab直接替换成相应数量的空格后存储的。这样,就使得在其他软件下看起来像是对齐的情况,交Python处理时变成未对齐,或者反之。假设某IDE设定,tab将做1到4补到4的整倍数处理,则上边的计算结果将发生变化:

ssss:1+1+1+1=4个空格
tttt:4+4+4+4=16个空格
stsstssstsssst:1+3+1+1+2+1+1+1+1+1+1+1+1+4=20个空格

后2个计算结果不再相同。但如果能掌握其他软件在处理tab的全部规则,则即便混用,也能写出正确无误的代码。

  Python允许混用tab和空格作为缩进的原因,应该是考虑到一个大程序,其源代码分为多个部分,有可能分别来自不同的人和软硬件环境,很难要求大家都统一只使用tab或空格。后面我们会看到,其实如果每个程序片段内部的tab和空格混用无误,多个程序组合后,仍然能够保证正常无误。

 

 

  • 其他心得与感想
  1. 想解决专业一点的编程类问题,还得搜英文网页,且能用Google、Bing就别用百度,能用Wikipedia就别用百度百科,能用Stackoverflow就别用百度知道,知乎还强点。
  2. 国人碰到难题后能够坚韧不拔解决问题的比较少,大环境如此。 

https://docs.python.org/2/reference/lexical_analysis.html#indentation

https://docs.python.org/2/reference/compound_stmts.html

posted on 2016-09-08 00:24  LiouPerng  阅读(1487)  评论(1)    收藏  举报