Skip to main content

迭代

更新变量

赋值语句中一种常见的模式是更新变量的赋值语句,其中变量的新值取决于旧值。

x = x + 1

这意味着“获取 x 的当前值,加 1,然后用新值更新 x。”

如果你试图更新一个不存在的变量,你会得到一个错误,因为 Python 在将值赋给 x 之前会先计算右侧:

>>> x = x + 1
NameError: name 'x' is not defined

在更新变量之前,你必须先初始化 (initialize) 它,通常用一个简单的赋值语句:

>>> x = 0
>>> x = x + 1

将变量加 1 称为递增 (increment);减 1 称为递减 (decrement)。

while 语句

计算机通常用于自动化重复性任务。不出错地重复相同或相似的任务是计算机擅长而人类不擅长的事情。因为迭代非常普遍,Python 提供了几种语言特性来简化它。

Python 中的一种迭代形式是 while 语句。这里有一个简单的程序,它从五开始倒数,然后说“发射!”。

n = 5
while n > 0:
print(n)
n = n - 1
print('Blastoff!')

你几乎可以像读英语一样读懂 while 语句。它的意思是:“当 n 大于 0 时,显示 n 的值,然后将 n 的值减 1。当你到 0 时,退出 while 语句并显示单词 Blastoff!

更正式地说,while 语句的执行流程如下:

  1. 评估条件,得到 TrueFalse
  2. 如果条件为假,退出 while 语句并继续执行下一条语句。
  3. 如果条件为真,执行循环体,然后返回步骤 1。

这种类型的流程称为循环 (loop),因为第三步会循环回到顶部。我们称每次执行循环体为一次迭代 (iteration)。对于上面的循环,我们会说,“它有五次迭代”,这意味着循环体被执行了五次。

循环体应该改变一个或多个变量的值,以便最终条件变为假并且循环终止。我们称那个每次循环执行时都会改变并且控制循环何时结束的变量为迭代变量 (iteration variable)。如果没有迭代变量,循环将永远重复,导致无限循环 (infinite loop)。

无限循环

程序员们一个永恒的乐趣来源是观察到洗发水说明上的“揉搓、冲洗、重复”是一个无限循环,因为没有迭代变量告诉你需要执行多少次循环。

countdown 的例子中,我们可以证明循环会终止,因为我们知道 n 的值是有限的,而且我们可以看到 n 的值每次循环都会变小,所以最终我们必然会到达 0。其他时候,一个循环明显是无限的,因为它根本没有迭代变量。

有时你直到执行到循环体的一半时才知道是时候结束循环了。在这种情况下,你可以故意编写一个无限循环,然后使用 break 语句跳出循环。

这个循环显然是一个无限循环,因为 while 语句上的逻辑表达式就是逻辑常量 True

n = 10
while True:
print(n, end=' ')
n = n - 1
print('Done!')

如果你犯了这个错误并运行了这段代码,你会很快学会如何在你的系统上停止失控的 Python 进程,或者找到你电脑的关机按钮在哪里。这个程序将永远运行下去,或者直到你的电池耗尽,因为循环顶部的逻辑表达式永远为真,因为它就是常量值 True

虽然这是一个功能失调的无限循环,但我们仍然可以使用这种模式来构建有用的循环,只要我们仔细地在循环体中添加代码,在达到退出条件时使用 break 显式退出循环。

例如,假设你想接受用户的输入,直到他们输入 done。你可以这样写:

while True:
line = input('> ')
if line == 'done':
break
print(line)
print('Done!')

# 代码: https://www.py4e.com/code3/copytildone1.py

循环条件是 True,它永远为真,所以循环会重复运行,直到遇到 break 语句。

每次循环,它都会用一个尖括号提示用户。如果用户输入 donebreak 语句会退出循环。否则,程序会回显用户输入的内容,并返回到循环的顶部。下面是一个示例运行:

> hello there
hello there
> finished
finished
> done
Done!

这种编写 while 循环的方式很常见,因为你可以在循环中的任何地方检查条件(不仅仅是在顶部),并且你可以用肯定的方式表达停止条件(“当这个发生时停止”),而不是否定的方式(“继续直到那个发生”)。

使用 continue 结束迭代

有时你在循环的一次迭代中,想要结束当前迭代并立即跳转到下一次迭代。在这种情况下,你可以使用 continue 语句跳到下一次迭代,而无需完成当前迭代的循环体。

下面是一个循环示例,它复制其输入直到用户输入“done”,但将以哈希字符开头的行视为不打印的行(有点像 Python 注释)。

while True:
line = input('> ')
if line[0] == '#':
continue
if line == 'done':
break
print(line)
print('Done!')

# 代码: https://www.py4e.com/code3/copytildone2.py

以下是添加了 continue 的新程序的示例运行。

> hello there
hello there
> # don't print this
> print this!
print this!
> done
Done!

所有的行都被打印出来了,除了以哈希符号开头的那一行,因为当 continue 被执行时,它结束了当前的迭代并跳回到 while 语句开始下一次迭代,从而跳过了 print 语句。

使用 for 的确定性循环

有时我们想要遍历一组事物,例如一个单词列表、文件中的行或一个数字列表。当我们有一个要遍历的事物列表时,我们可以使用 for 语句构建一个确定性 (definite) 循环。我们将 while 语句称为不确定性 (indefinite) 循环,因为它只是循环直到某个条件变为 False,而 for 循环是遍历一个已知的项目集合,所以它运行的迭代次数与集合中的项目数量一样多。

for 循环的语法与 while 循环类似,都有一个 for 语句和一个循环体:

friends = ['Joseph', 'Glenn', 'Sally']
for friend in friends:
print('Happy New Year:', friend)
print('Done!')

用 Python 的术语来说,变量 friends 是一个包含三个字符串的列表 1for 循环遍历该列表,并对列表中的三个字符串各执行一次循环体,从而产生以下输出:

Happy New Year: Joseph
Happy New Year: Glenn
Happy New Year: Sally
Done!

将这个 for 循环翻译成中文不像 while 那样直接,但如果你把 friends 看作一个集合,那么它就是这样的:“对于名为 friends 的集合中的每个 friend,运行 for 循环主体中的语句一次。”

观察 for 循环,forin 是 Python 的保留关键字,而 friendfriends 是变量。

for friend in friends:
print('Happy New Year:', friend)

特别地,friend 是 for 循环的迭代变量。变量 friend 在循环的每次迭代中都会改变,并控制 for 循环何时完成。迭代变量依次遍历存储在 friends 变量中的三个字符串。

循环模式

我们经常使用 forwhile 循环来遍历一个项目列表或文件的内容,并且我们正在寻找某些东西,例如我们扫描的数据中的最大值或最小值。

这些循环通常通过以下方式构建:

  • 在循环开始前初始化一个或多个变量
  • 在循环体中对每个项目执行一些计算,可能会在循环体中更改变量
  • 在循环完成时查看结果变量

我们将使用一个数字列表来演示这些循环模式的概念和构建。

计数和求和循环

例如,要计算列表中项目的数量,我们会编写以下 for 循环:

count = 0
for itervar in [3, 41, 12, 9, 74, 15]:
count = count + 1
print('Count: ', count)

我们在循环开始前将变量 count 设置为零,然后我们编写一个 for loop 来遍历数字列表。我们的迭代变量名为 itervar,虽然我们在循环中没有使用 itervar,但它确实控制着循环,并使循环体对列表中的每个值执行一次。

在循环体中,我们对列表中每个值将 count 的当前值加 1。当循环执行时,count 的值是我们“到目前为止”看到的值的数量。

一旦循环完成,count 的值就是项目的总数。总数在循环结束时“落入我们手中”。我们构建循环,使得当循环结束时我们拥有我们想要的东西。

另一个计算一组数字总和的类似循环如下:

total = 0
for itervar in [3, 41, 12, 9, 74, 15]:
total = total + itervar
print('Total: ', total)

在这个循环中,我们确实使用了迭代变量。与前一个循环中简单地将 count 加一不同,我们在每次循环迭代期间将实际的数字(3、41、12 等)加到运行总和中。如果你考虑变量 total,它包含“到目前为止值的运行总和”。因此,在循环开始之前,total 为零,因为我们还没有看到任何值;在循环期间,total 是运行总和;在循环结束时,total 是列表中所有值的总和。

随着循环的执行,total 累积元素的总和;以这种方式使用的变量有时被称为累加器 (accumulator)。

计数循环和求和循环在实践中都不是特别有用,因为有内置函数 len()sum() 分别计算列表中的项目数和列表中项目的总和。

最大值和最小值循环

要找到列表或序列中的最大值,我们构建以下循环:

largest = None
print('Before:', largest)
for itervar in [3, 41, 12, 9, 74, 15]:
if largest is None or itervar > largest :
largest = itervar
print('Loop:', itervar, largest)
print('Largest:', largest)

当程序执行时,输出如下:

Before: None
Loop: 3 3
Loop: 41 41
Loop: 12 41
Loop: 9 41
Loop: 74 74
Loop: 15 74
Largest: 74

变量 largest 最好被理解为“我们到目前为止看到的最大值”。在循环之前,我们将 largest 设置为常量 NoneNone 是一个特殊的常量值,我们可以将其存储在变量中以标记该变量为“空”。

在循环开始之前,我们到目前为止看到的最大值是 None,因为我们还没有看到任何值。在循环执行期间,如果 largestNone,那么我们将看到的第一个值作为迄今为止的最大值。你可以在第一次迭代中看到,当 itervar 的值是 3 时,由于 largestNone,我们立即将 largest 设置为 3。

第一次迭代之后,largest 不再是 None,因此复合逻辑表达式的第二部分,即检查 itervar > largest,仅在我们看到大于“迄今为止最大值”的值时才会触发。当我们看到一个新的“更大”的值时,我们将那个新值赋给 largest。你可以在程序输出中看到 largest 从 3 变为 41 再变为 74。

在循环结束时,我们已经扫描了所有的值,变量 largest 现在确实包含了列表中的最大值。

要计算最小值,代码非常相似,只有一个小小的改动:

smallest = None
print('Before:', smallest)
for itervar in [3, 41, 12, 9, 74, 15]:
if smallest is None or itervar < smallest:
smallest = itervar
print('Loop:', itervar, smallest)
print('Smallest:', smallest)

同样,smallest 是循环执行之前、期间和之后的“迄今为止最小值”。当循环完成时,smallest 包含列表中的最小值。

再次强调,就像计数和求和一样,内置函数 max()min() 使得编写这些精确的循环变得没有必要。

以下是 Python 内置 min() 函数的一个简化版本:

def min(values):
smallest = None
for value in values:
if smallest is None or value < smallest:
smallest = value
return smallest

在最小值代码的函数版本中,我们删除了所有的 print 语句,使其等同于 Python 中已经内置的 min 函数。

调试

当你开始编写更大的程序时,你可能会发现自己花费更多时间在调试上。更多的代码意味着更多出错的机会和更多隐藏 bug 的地方。

缩短调试时间的一种方法是“二分法调试”(debugging by bisection)。例如,如果你的程序有 100 行,你逐一检查它们,需要 100 步。

相反,尝试将问题一分为二。查看程序的中间部分或其附近,寻找可以检查的中间值。添加一个 print 语句(或其他具有可验证效果的东西)并运行程序。

如果中点检查不正确,问题一定出在程序的前半部分。如果正确,问题就在后半部分。

每次你进行这样的检查,你都将需要搜索的行数减半。经过六步(远少于 100 步),你至少在理论上可以将范围缩小到一两行代码。

实际上,并不总是清楚什么是“程序的中间部分”,也并非总是能够检查它。计算行数并找到精确的中点没有意义。相反,考虑程序中可能出错的地方以及容易放置检查点的地方。然后选择一个你认为 bug 在检查点之前或之后的几率大致相同的位置。

术语表

累加器 (accumulator) 在循环中用于累加结果的变量。 计数器 (counter) 在循环中用于计算某事发生次数的变量。我们将计数器初始化为零,然后在每次想要“计数”某事时递增计数器。 递减 (decrement) 减少变量值的更新操作。 初始化 (initialize) 为将要更新的变量赋予初始值的赋值操作。 递增 (increment) 增加变量值的更新操作(通常是加一)。 无限循环 (infinite loop) 终止条件永远不满足或没有终止条件的循环。 迭代 (iteration) 使用函数调用自身或使用循环重复执行一组语句。

练习

练习 1: 编写一个程序,重复读取整数,直到用户输入“done”。一旦输入“done”,打印出整数的总和、计数和平均值。如果用户输入的不是整数,使用 tryexcept 检测他们的错误,打印错误消息,并跳到下一个整数。

Enter a number: 4
Enter a number: 5
Enter a number: bad data
Invalid input
Enter a number: 7
Enter a number: done
16 3 5.333333333333333

练习 2: 编写另一个程序,像上面一样提示输入一个数字列表,并在最后打印出数字的最大值和最小值,而不是平均值。


  1. 我们将在后面的章节中更详细地研究列表。 ↩︎

如果你在本书中发现错误,欢迎使用 Github 给我发送修正。