Python极简讲义:一本书入门数据分析与机器学习
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.3 程序控制结构

在前面的章节中,我们讨论了Python的基本数据类型。在此基础之上,我们将开始介绍计算机程序常用的控制结构。首先,我们简要回顾一下程序控制结构的发展历史。

2.3.1 回顾那段难忘的历史

结构化程序设计(Structured Programming)是一种经典的编程模式,在20世纪60年代开始发展,这种思想最早是由荷兰著名计算机科学家、图灵奖得主艾兹格·W·迪科斯彻(E.W.Dijkstra)提出的。Dijkstra设计了一套规则,使程序设计具有合理的结构,以保证程序的正确性。这套规则要求程序设计者按照一定的结构形式来设计和编写程序,而不是“天马行空”地根据自己的意愿来编写。

1966年,Böhm和Jacopini等人提出了结构化程序理论Böhm C, Jacopini G.Flow diagrams, turing machines and languages with only two formation rules[J].Communications of the ACM, 1966, 9(5): 366-371.。他们的研究结论是,只要一种编程语言利用三个控制方式组合其子程序及调整控制流程,则每个可计算函数都可以用此种编程语言来表示。这三个调整控制流程的方式如下。

●运行一个子程序,然后接着运行下一个(顺序)。

●依照布尔变量的结果,选择运行两个子程序中的一个(选择)。

●重复运行某个子程序,直到特定布尔变量为True才结束(循环)。

早期的程序员可没有结构化编程思想,他们广泛使用GOTO语句(即跳转到指定标签位置的一种程序控制策略)。GOTO语句也称为无条件转移语句,它的优点在于“指哪打哪”,效率非常高。

但GOTO语句的缺点也很明显,那就是破坏了程序设计的结构性,导致程序流程混乱,使理解和调试程序都产生困难。1966年5月,Dijkstra在著名学术期刊Communications of the ACM上发表论文并指出Dijkstra E W.Go to statement considered harmful[M]//Software pioneers.Springer Berlin Heidelberg, 2002: 351-355.,任何一个有GOTO指令的程序,都可以改为完全不使用GOTO指令的程序,即“所有有意义的程序流程都可以由三种基本的结构构成”。

1968年,Dijkstra发表了著名的论文《GOTO语句有害论》(Go To Statement Considered Harmful)(见图2-6)。

图2-6 Dijkstra与他的经典论文《GOTO语句有害论》

在这篇论文中,Dijkstra犀利地指出:“几年前我就观察到,一个程序员的质量,与其程序中GOTO语句的密度成反比。”他还阐述道:“后来我发现了为什么使用GOTO语句有这么严重的后果,并相信所有高级语言都应该把GOTO废除掉。”

立足现在,回望过往,我们可能觉得Dijkstra真是高屋建瓴,具有真知灼见。可是,你知道吗,这篇论文在盲审时也被论文评阅人批得惨不忍睹。

其中一位评阅人的意见就是:“发表这样的论文,纯粹就是浪费纸张。这样的论文,它既不会被引用,也不会被人注意。我敢肯定,从现在起的30年内,GOTO语句不仅会活得好好的,而且还会像现在一样应用广泛。”原文为:Publishing this would waste valuable paper: Should it be published, I am as sure it will go uncited and unnoticed as I am confident that, 30 years from now, the goto will still be alive and well and used as widely as it is today.

这段有关Dijkstra论文发表的小故事告诉我们,即使你是金子,也有可能有被人误解为破铜烂铁,但结局总是完美的,是金子,终究还是会发光的。

Dijkstra的论文针砭时弊,引起了激烈的讨论。之所以激烈,是因为当时人们正忙于IBM 360系列大型机的研究,IBM 360使用的主要编程语言是FortranFortran源自“公式翻译”(即FormulaTranslation)的缩写,是世界上最早出现的计算机高级程序设计语言,广泛应用于科学和工程计算领域。,而GOTO语句则是Fortran的支柱之一。

但人们还是逐渐意识到,问题的关键不是简单地去掉GOTO语句,而是形成一种新的程序设计观念和风格,以期显著提高软件生产率,降低软件维护成本。

自此,人们的编程方式发生了重大变化,每种语言都提供三种基本控制结构的实现,并提供局部化数据访问的能力及某种形式的模块化编译机制。正是这个原因,在Python中,压根就没有提供GOTO这个程序控制策略。

在现代的编程设计中,不论是顺序结构、选择结构,还是循环结构,它们都有一个共同点——只有一个入口,也只有一个运行出口。在程序中,使用这些结构到底有什么好处呢?答案是,这些单一的入口、出口可以让程序可控、易读、好维护。下面我们分别介绍。

2.3.2 顺序结构

结构化程序中最简单的结构就是顺序结构。所谓顺序结构程序就是由按书写顺序执行的语句构成的程序段,其流程如图2-7(a)所示。

图2-7 程序的控制结构

通常情况下,顺序结构程序是按照语句出现的先后顺序一句一句执行的。前面的程序,大多数都属于顺序结构程序。

2.3.3 选择结构

有一些程序并不按顺序执行,这种情况称为“控制转移”,它涉及另外两类程序控制结构,即选择结构和循环结构。在许多处理实际问题的程序设计中,根据输入数据和中间结果的不同,需要选择不同的语句执行。在这种情况下,必须根据某个变量或表达式的值做出判断,以决定执行哪些语句,不执行哪些语句。

选择结构会根据给定的条件进行判断,决定执行哪个分支的程序段。条件分支不是我们常说的“兵分两路”。“兵分两路”是指两条路都有“兵”,而这里的条件分支在执行时“非此即彼”,不可兼得。我们要进行分支选择,由if语句和if-else语句来实现。

如图2-7(b)所示,if-else语句可以依据判断条件的结果来决定要执行的语句。当判断条件的值为Ture时执行语句块1;当判断条件的值为False时则执行语句块2。不论执行哪一个语句块,最后都会再回到“语句3”继续执行。

在Python中,多个语句构成代码组(suite)。通常,我们把缩进相同的一组语句称为代码组。像if、while、def和class这样的复合语句(后面的章节将会解释这些关键字),首行以关键字开始,以冒号结束,该行之后的一行或多行代码将构成代码组。

下面说明if-else语句的基本形式,如图2-8所示。其中“某个逻辑判断条件”成立时(即非零或非空),则执行if后面的语句组(后面的冒号“:”不可缺少),而执行内容可以为多行,以相同的缩进来区分同一隶属范围。else为可选语句(如果有该项,后面的冒号“:”亦不可少),在条件不成立时执行else下属的语句组。

图2-8 if-else语句的基本形式

需要注意的是,else一定要与前面的if对应,也就是说具有相同的缩进,以表示它们属于同一个语句组。此外,不同于C、C++、Java把逻辑判断条件用一对圆括号括起来,Python的所有逻辑判断条件(包括后面即将讲到的while循环)都不需要用到这对括号。具体参考如下代码的第03行。

在if语句的判断条件中,可以用>(大于)、<(小于)、==(等于)、>=(大于等于)、<=(小于等于)等表示比较对象的逻辑关系。

这里需要注意的是,“=”和“==”很容易混淆。一个等号“=”表示的是赋值,即将等号右侧的值赋给左侧变量(如a=b,表示把变量b的值赋给a)。相比而言,两个等号“==”表示的是逻辑判断,比较“==”两侧的对象是否相等(如a==b,表示判定a和b是否相等,如果相等,返回True,否则返回False)。

当判断条件有多个时,可以使用以下形式。

这里需要注意的是,在判断是否满足条件时,不同于C、C++等语言中的关键词“else if”,Python对关键词做了简化,为“elif”。自然,分支语句也是可以嵌套的,但需要注意,同一级别的if-else,一定要保证有相同的缩进关系。

此外,由于Python并不支持C、C++中常见的switch语句,所以多个条件的判断,只能用elif来实现。如果需要依据多个条件来进行逻辑判断,可以借助关键字and(与)、or(或)及not(非)等,将多个条件“合纵连横”。例如,and表示只有多个条件同时成立时判断条件才成立;or表示多个条件中有一个成立时判断条件成立;not表示否定原来的逻辑判断,若原来逻辑为True,经not操作后则为False,反之亦然。请参考【范例2-1】。

【范例2-1】多条件判断(if-else.py)

【运行结果】

【代码解析】

以上代码逻辑简单,注释清楚,无须赘言。需要说明的是,对于第02行代码,Python提供了不错的语法糖,可改用被注释了的第03行代码,它的逻辑判断描述就更加接近于数学语言描述了。

此外,还需注意的是,在布尔判断中,除了对数值型变量判断时有“非零即为真”这样的规则,Python中的None、空字符串、空列表、空字典、空元组都相当于False。所以【范例2-1】中的第21行定义了一个空字典,第22行通过not进行了否定,让逻辑判断变为True,从而得到对应的输出:“这是一个空字典!”

对于简单的if-else语句,推崇“简洁即是美”理念的Python,还提供了条件表达式的三元操作符对于编程语言而言,当我们说“多少元”时,实际上是说某个操作符同时操作了多少个数。比如“-3”中的负号“-”就是一元操作符,因为它只有一个操作数“3”。再比如,“3+4”中的加号“+”是一个二元操作符。。三元操作符的语法如下。

它表示如果某个条件成立,那么a=x,否则a=y。下面我们参考【范例2-2】。

【范例2-2】if-else的三元操作符(if-ternary.py)

【运行结果】

【代码解析】

被注释的第04~07行,其功效完全等同于第09行。客观来讲,只有比较简短的if-else语句才值得被这么“简化”,否则,还是回归到常规的if-else表达方式,那样更具有可读性。

2.3.4 循环结构

在有些情况下,我们还会重复做一件有规律的事情。比如,我们想逐个输出列表中的元素,然后再据此做一些事情,这时我们就需要利用循环结构。循环结构的特点是,在给定条件成立时,反复执行同一个程序段。

通常,我们称给定条件为“循环条件”,称反复执行的程序段为“循环体”。循环体可以是复合语句,也可以是单个语句。循环体中也可以包含循环语句,实现循环的嵌套。循环结构的流程如图2-9所示。

图2-9 循环结构

循环结构包括for循环和while循环,还可以使用嵌套循环完成复杂的程序控制操作。下面我们先来介绍for循环。

2.3.4.1 for循环

我们可利用for循环依次把列表或元组中的每个元素迭代取出,并做相应的操作。示例代码如下。

这里简单介绍一下在Python中利用for循环处理任意大小列表的方式,具体的使用细节如图2-10所示(图中的缩进可以是一个Tab键,也可以是四个空格,只要保证缩进的尺度相同即可)。

图2-10 用for循环处理列表

这里的关键词“in”等同于把列表中的每个元素逐个取出,并赋值给目标识别符所代表的变量。事实上,“for…in”循环可以作用于任何可迭代的序列,而不仅仅适用于列表,代码如下所示。

在上述代码中,控制for循环次数的是内置函数range(),该函数可创建一个整数列表,一般用在for循环中。该函数的原型如下。

该函数的参数说明如下。

●start:计数从start开始,默认是从0开始,例如range(10)等价于range(0,10)。

●stop:计数到stop结束,但不包括stop,例如range(0, 10)返回的列表是[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],并不包含10。

●step:步长,默认为1,例如range(0, 5)等价于range(0, 5, 1)。

在Python的for循环中,我们还可以同时提取多个变量来完成给定的操作。下面的示例就用到了前面提到的内置函数enumerate(),参见图2-11中的代码。

图2-11 多变量的for循环

在图2-11的In [5]处,我们利用enumerate()函数将一个列表打包成了一个个元组对(索引值,元素值),每个元组中有两个值,如(0, a)、(1, b)等。于是,我们需要两个变量分别来接收这两个值,代码中用到的是index和key,它们分工明确,靠前的index接收enumerate()给出的索引值,靠后的key接收原有列表的字符。

这里还涉及另外一个语法点,即for循环内部的print打印格式。在Python中,格式化输出字符串时通常使用字符串类提供的format()方法。

如前所述,在Python中,一切皆对象。所以,在print语句中,严格来说,"seq [{0}]={1}"是一个字符串对象。既然是对象,那么我们就可以通过点(.)操作符访问它的成员方法,这里的方法就是format()。关于format()方法的使用,前面的章节中已有讨论,这里不再赘述。

2.3.4.2 while循环

除了可利用for循环来完成需要重复处理的相同任务,我们还可以使用while循环来实现类似的功能。它的语法格式如下。

示例代码如下。

为了让读者对while循环有更多感性认识,我们再列举一个判断列表元素奇偶性的复合小程序,参见【范例2-3】。

【范例2-3】利用while循环判断列表元素的奇偶性(while-loop.py)

【运行结果】

【代码解析】

本例把前面学到的if-else和while循环结合起来了。第02行和第03行分别创建了两个空列表。如果我们利用前面学到的多赋值规则,可以把这两行代码合并为1行,具体如下。

第05行利用了列表对象的pop()方法。该方法的默认索引值index为-1,即弹出倒数第1个元素,然后赋值为num。后面的if-else框架则根据num的奇偶性,利用列表的append()方法分别将元素添加到even或odd列表中。

2.3.4.3 跳出循环

在佛教中,常有“超出三界外,不在五行中”的说法,意为摆脱某种循环周始的羁绊,方得解脱。在程序设计中,我们也常有类似的需求。在满足某些条件时,我们希望跳出for循环或while循环,这时就需要借助break、continue等语句。它们都是用来控制程序流程转向的,但在执行细节上是有区别的。

break语句也称为中断语句,它通常用来在适当的时候直接退出循环,执行循环之外的语句,如图2-12所示。

图2-12 break语句

对于图2-12中所示的代码,其完成的功能是,打印出1~50后,紧接着跳转到print语句,打印END,程序结束。由此可见,break语句的作用是提前结束本层循环。如果是嵌套循环,break语句可跳出内层循环,执行外层循环。

相比于break语句,continue语句的功能有所不同,它是在满足条件时,仅仅跳过continue后面的余下部分,提前进入下一轮循环,如图2-13所示。

图2-13 continue语句

对于图2-13中的代码,其功能是输出1~10中的奇数,即1、3、5、7、9。当n为偶数时,continue语句后面的print(n)不执行,直接开始下一轮循环。可见continue的作用是,提前结束本轮循环,整个循环的次数,其实一次都没有少,不过是部分循环并没有执行完罢了(以continue为分割线)。

总结一下,continue是“向上跳”,跳不出如来掌心,仍然还在循环体内。break是“向下跳”,跳出“三界外”,脱离循环体。