3.4.2 while 循环
for 循环要求预先确定循环的次数,但有很多问题难以预先确定循环次数,只知道在什么 条件下需要循环,这时可以使用 while 语句。Python 语言中 while 语句的常用格式是:
while <布尔表达式>:
<循环体>
其语义是:当布尔表达式计算为 True 时,执行一遍循环体,执行完毕控制转回 while 语句的开始处重新测试布尔表达式;当布尔表达式计算为 False 时,控制转向下一条语句。注意, 循环体部分相对于 while 部分要左缩进。while 语句的流程图如图 3.9 所示。
图 3.9 while 循环的流程图
显然,while 语句的循环次数取决于布尔表达式何时变成 False,而不是预先确定循环次 数。稍微深入思考一下就会发现一个问题:万一布尔表达式永远不会变成 False 怎么办?如 果进入循环语句时布尔表达式计算为 True,而循环体又不会影响布尔表达式的值,那么执行 完循环体后控制回到循环入口处时,布尔表达式仍然为 True。这种情况下 while 循环永远不 会停下来,称为无穷循环。程序中如果有一个无穷循环就意味着程序无法终止,我们常说程 序进入了“死循环”,这是程序设计中经常出现的错误。要想避免无穷循环,必须使循环体对 布尔表达式的值产生影响,例如改变布尔表达式中所用到的变量的值。
在实际编程中,while 循环有一些常用的套路或称模式,值得读者熟记于心。下面通过“对 一批数据求和”的例子来介绍这些循环模式。
交互式循环
考虑这样的应用:用户不断输入数据,程序得到数据后不断累加,最后算出输入数据的 总和。显然,这是一个“输入——累加”的反复循环的过程。由于用户输入数据的个数不是 预先给定的,故无法用 for 循环来实现,而 while 语句则能轻松解决这个问题。为了对循环进 行控制,我们每次循环前都询问用户是否还有新数据。这种通过与用户进行交互来决定是否 需要循环的模式称为交互式循环,可以用伪代码表达如下:
将循环控制变量 moredata 初始化为"yes"
while moredata 值为"yes":
获得用户输入的下一个数据
处理该数据
询问用户是否还有输入数据,为变量 moredata 赋值
下面是利用交互式循环实现的完整程序。
【程序 3.10】eg3_10.py
sum = 0 moredata = "yes"
while moredata[0] == "y":
x = input("Input a number: ")
sum = sum + x
moredata = raw_input("More numbers? (yes/no) ")
print "The sum is", sum
下面是这个程序的执行情况:
Input a number: 2
More numbers? (yes/no) yes
Input a number: 5
More numbers? (yes/no) yeah
Input a number: 8
More numbers? (yes/no) no
The sum is 15
要说明的是,这个程序通过检查 moredata[0]来控制是否循环,因此只要用户输入的首字 母是”y”就能进入循环体执行,而任何非 y 开头的输入都导致停止循环。
此版本有个不好的地方:用户需要不停地先回答是否还有数据,有的话再输入数据。这 对用户来说有点烦琐。也就是说,交互式循环其实并不适合“输入 n 个数据求和”的问题。
哨兵循环
解决“输入数据求和”问题的更好方法是使用哨兵循环,即不断执行“输入——累加” 这个循环体,直至遇到一个称为“哨兵”的特殊数据值。任何值都可以当作哨兵,关键是它 必须与正常数据值相互区别。哨兵循环的一般模式如下:
前导输入
while 不是哨兵:
处理数据
循环尾输入
首先,在循环开始之前需要利用“前导输入”获取第一个数据。如果该数据是哨兵,则 不会进入循环,控制转向 while 的下一条语句;如果是正常数据,则进入循环处理之。在循 环体的末尾读取下一个数据,并转到循环开始处的哨兵测试。如此周而复始,直至遇见哨兵 循环才终止。
注意,哨兵循环用到了两条一模一样的输入语句:一条是位于循环体之前的“前导输入”, 另一条是位于循环体末尾的“循环尾输入”。这两条输入语句缺一不可:少了前导输入则无法 进入循环;少了循环尾输入则无法读取下一数据,从而循环一直在对第一个数据进行处理, 导致无穷循环。
使用哨兵循环,需要选择一个特殊数据作为哨兵。这个特殊数据一般和正常数据属于同 样的类型,以便能被 while 语句统一检测。如果哨兵数据和正常数据属于不同类型,那么 while 语句的条件表达式就会变复杂,因为需要处理两种类型的数据。具体选择什么数据作为哨兵,要看具体的应用场景。例如,假设我们输入的数据是考试成绩(非负数),那么可以选-1 作 为哨兵,因为负数是不可能成为合法考试成绩的。又假如我们输入的数据是人名(字符串), 那么可以选空串作为哨兵。一个很常用的场景是文件处理,即输入的数据来自一个文件,这 时可以很自然地在文件末尾存放一个特殊数据作为哨兵,很多语言甚至提供专门的测试文件 尾的手段(如常量 EOF①或函数 eof()之类)。关于文件处理详见第 6 章。
下面我们用哨兵循环来实现求和程序。
【程序 3.11】eg3_11.py
sum = 0
x = input("Input a number (-1 to quit): ")
while x >= 0:
sum = sum + x
x = input("Input a number (-1 to quit): ")
print "The sum is", sum
可见哨兵循环与交互式循环不同,不需要用户不停地回答 yes 来处理数据。下面是此程序的执行情况:
Input a number (-1 to quit): 2
Input a number (-1 to quit): 5
Input a number (-1 to quit): 8
Input a number (-1 to quit): -1
The sum is 15
如果程序 3.11 中待求和的数据是任意实数的话,那么-1 可能是正常数据,不能作为哨
兵。事实上,所有实数都不能作为哨兵。这时可以采用字符串类型来解决问题,因为正常的 输入数据(正数、负数和 0)都可以表示为由阿拉伯数字组成的非空字符串,从而包含非数 字字符的字符串都可以用作哨兵。最简单最常用的特殊字符串是空字符串””(注意两个引号 之间没有东西),当用户在输入时直接键入回车,Python 即返回空串。当然,这种做法中对 数据的处理要麻烦一点,需要将字符串转换为数值类型以便进行求和计算。下面是以字符串 方式输入数值数据的求和程序版本:
【程序 3.12】eg3_12.py
sum = 0
x = raw_input("Input a number (<Enter> to quit): ")
while x != "":
sum = sum + eval(x)
x = raw_input("Input a number (<Enter> to quit): ")
print "The sum is", sum
执行示例如下:
Input a number (<Enter> to quit): 2
Input a number (<Enter> to quit): 5
Input a number (<Enter> to quit): -8
Input a number (<Enter> to quit):
The sum is -1
此运行示例的第四行中,输入时直接按回车键,导致 Python 将空串赋值给了 x,从而使循环 终止。
① 意为 End-Of-File
在哨兵循环模式中有一个容易犯错误的地方:当用户的前导输入本身就是哨兵,从而导致循环一次也不执行时,while 后面的语句可能没有预料这种情况,导致无法正确执行。例如, 我们将程序 3.11 改成计算平均值,算法基本不变:反复输入数据,在循环中累加数据总和 sum 及数据计数 count,当用户输入哨兵-1 时退出循环并计算平均值。代码如下:
sum = 0
count = 0
x = input("Input a number (-1 to quit): ")
while x >= 0:
sum = sum + x count = count + 1
x = input("Input a number (-1 to quit): ")
print "The average is", sum / count
运行此程序,并且首先输入-1,看看会发生什么?是的,由于未进入循环,sum 和 count 都保持为初始值 0,while 的下一条语句在计算平均值的时候发生了除数为 0 的错误!
后测试循环
前面介绍的 while 循环例子都是“前测试”循环,即先检测循环条件,再进入循环。显 然,如果首次测试条件得到 False,则循环体就会一次也不执行。有些实际应用问题要求循环 体必须至少执行一次,例如“输入合法性检查”问题。用户输入数据,程序检查用户的输入 是否合法:如果合法则程序继续向后执行,否则就回到前面要求用户重新输入,直至输入合 法为止。这种输入合法性检查在程序设计中是非常普遍的,好的程序员应该尽量对用户的输 入进行合法性检查。为了实现输入合法性检查,显然需要先获得用户的输入,然后进入循环 语句,即循环至少会执行一次,最后再测试条件决定是否继续循环。因此,我们需要一种“后 测试”循环结构。
有一些语言提供了 repeat-until 或者 do-while 循环结构来实现后测试循环,其语义分别是 “重复做某事,直至满足某条件”和“做某事,当满足条件时重复”。这种循环结构的流程图 如图 3.10 所示①。
图 3.10 后测试循环控制结构
从图中可见循环体至少执行一次,而循环条件检测位于循环体的最后,这正是“后测试”名称的由来。
① 细微差别:这个流程图其实是 repeat-until 结构。将 True 和 False 互换位置,才是 do-while 结构。
Python 语言没有提供类似 repeat-until 结构的语句,但我们不难用 while 来实现后测试循环:只要保证循环条件初始为 True,自然就会执行一次循环体,而后续循环由条件测试决定。 例如,假设程序要求用户输入一个正数,则可用下面的代码片段来检查输入合法性:
x = -1
while x < 0:
x = input("Please input a positive number: ")
其中第一行的作用是使首次条件检测为真,因此循环体至少执行一次;接下去的条件检测就 相当于位于循环体最后,整个语句也就等价于后测试循环。
不难看出,程序 3.10 中的交互式循环将 moredata 初始化为 True,因此实际上是后测试 循环的一种,效果是使用户至少输入一个数据。
while 计数器循环
虽然一般来说 for 语句用于固定次数的循环,while 语句用于不定次数的循环,但两者之 间并无本质不同,完全可以用 while 来实现固定次数的循环。为了循环 n 次,用 while 实现的 计数器循环模式如下:
计数器 count 置为 0
while count < n:
处理代码
count = count + 1
可见,用 while 语句实现计数器循环时需要手动控制循环控制变量 count 的变化,而 for 语句是自动处理的。使用时具体要注意两点:第一,必须为 count 赋初值,否则 while 后的布 尔表达式无法计算;第二,必须在循环体中改变 count 的值,否则会导致无穷循环。
例如,前面罚写 10 遍“烦”字的计数器循环,可以用 while 语句实现如下:
>>> i = 0
>>> while i < 10:
print "烦",
i = i + 1
烦 烦 烦 烦 烦 烦 烦 烦 烦 烦
在此例中,i 初值为 0,以后每次循环都加 1,由于从 0 到 9 都是满足循环条件的,所以 总共执行 10 次循环。第 10 次循环后,i 值为 10,不满足循环条件,故退出循环,控制转到 下一条语句。
上面的 while 计数器循环模式是让计数器从小到大变化,同样常用的还有让计数器从大 到小变化的模式:
计数器 count 置为 n
while count > 0
处理代码
count = count - 1
使用这种模式实现上面的例子,代码如下:
>>> i = 10
>>> while i > 0:
print "烦", i = i - 1
烦 烦 烦 烦 烦 烦 烦 烦 烦 烦
值得一提的是,计算机编程中我们经常是从 0 开始计数的,而不是日常生活中的从 1 开始。上例中的 while 语句都是对从 0 到 9 的 10 个值进行循环。如果读者更习惯从 1 开始计数, 也可以先为 count 赋予初值 1,然后在循环体中不断加 1,从而实现对从 1 到 10 的 10 个值进 行循环,这样能与日常说的第 1 次、第 2 次、…、第 10 次在序数上对应起来。但是要注意的 是,这时需要将循环条件改成 count<=10 或 count<11。循环控制变量的边界值是初学者容易 犯错的地方,经常导致循环次数多 1 次或少 1 次。