Python-自动化指南-繁琐工作自动化-第三版-二-
Python 自动化指南(繁琐工作自动化)第三版(二)
原文:
automatetheboringstuff.com/译者:飞龙
3 循环

在上一章中,你学习了如何使程序运行某些代码块而跳过其他代码。但流控制不仅仅是这些。在本章中,你将学习如何使用循环重复执行代码块。Python 的两种循环类型,while 和 for,开启了自动化全部功能,因为它们可以每秒运行数百万行代码。你还将学习如何导入代码库,称为 模块,以使更多函数可用于你的程序。
while 循环语句
你可以使用 while 语句使代码块重复执行。while 子句中的代码会在条件为 True 时执行。在代码中,while 语句始终由以下内容组成:
-
while关键字 -
一个条件(即评估为
True或False的表达式) -
一个冒号
-
从下一行开始,一个缩进的代码块(称为
while子句或while块)
你可以看到 while 语句看起来与 if 语句相似。区别在于它们的行为。在 if 子句的末尾,程序执行在 if 语句之后继续。但在 while 子句的末尾,程序执行会跳回到 while 语句的开始。while 子句通常被称为 while 循环 或简称为 循环。
让我们看看一个使用相同条件并基于该条件执行相同操作的 if 语句和 while 循环。以下是带有 if 语句的代码:
spam = 0
if spam < 5:
print('Hello, world.')
spam = spam + 1
这是带有 while 语句的代码:
spam = 0
while spam < 5:
print('Hello, world.')
spam = spam + 1
这些语句是相似的;if 和 while 都会检查 spam 的值,如果它小于 5,它们会打印一条消息。但是当你运行这两个代码片段时,每个都会发生非常不同的事情。对于 if 语句,输出只是 "Hello, world."。但对于 while 语句,它是 "Hello, world." 重复五次!查看这两个代码片段的流程图,图 3-1 和 3-2,以了解为什么会这样。

图 3-1:if 语句代码的流程图描述
带有 if 语句的代码检查条件,如果该条件为真,则只打印一次 "Hello, world."。另一方面,带有 while 循环的代码会打印五次。循环在打印五次后停止,因为每次循环迭代结束时 spam 中的整数会增加一个,这意味着循环会在 spam < 5 为 False 之前执行五次。

图 3-2:while 语句代码流程图说明
在 while 循环中,条件总是在每次 迭代(即每次循环执行时)的开始处进行检查。如果条件为 True,则执行子句,然后再次检查条件。第一次发现条件为 False 时,将跳过 while 子句。
一个烦人的 while 循环
这是一个小型的示例程序,它会不断地要求你输入,字面意思是输入你的名字。选择 文件新建 打开一个新的文件编辑窗口,输入以下代码,并将文件保存为 yourName.py:
name = ''
while name != 'your name':
print('Please type your name.')
name = input('>')
print('Thank you!')
首先,程序将 name 变量设置为空字符串。这样,name != 'your name' 条件将评估为 True,程序执行将进入 while 循环的子句。
这个子句中的代码要求用户输入他们的名字,并将其分配给 name 变量。由于这是代码块中的最后一行,执行将回到 while 循环的开始并重新评估条件。如果 name 中的值不等于字符串 'your name',则条件为 True,执行将再次进入 while 子句。
但一旦用户确实输入了 your name,while 循环的条件将变为 'your name' != 'your name',这评估为 False。现在,程序执行不再重新进入 while 循环的子句,而是跳过它并继续运行程序的其余部分。图 3-3 显示了 yourName.py 程序的流程图。

图 3-3:yourName.py 程序的流程图说明
现在,让我们看看 yourName.py 的实际运行情况。按 F5 运行它,在你给出程序所需内容之前,先输入几次除 your name 之外的内容:
Please type your name.
>Al
Please type your name.
>Albert
Please type your name.
>%#@#%*(^&!!!
Please type your name.
>your name
Thank you!
如果你从未输入 your name,那么 while 循环的条件将永远不会变为 False,程序将永远不断地询问你问题。在这里,input() 调用允许用户输入正确的字符串,使程序继续运行。在其他程序中,条件可能实际上永远不会改变,这可能会成为一个问题。让我们看看如何跳出 while 循环。
break 语句
有一种快捷方式可以让程序执行提前跳出 while 循环的子句。如果执行到达 break 语句,它将立即退出 while 循环的子句。在代码中,break 语句仅包含 break 关键字。
这是一个与之前的 yourName.py 程序做相同事情但使用 break 语句来跳出循环的程序。输入以下代码,并将文件保存为 yourName2.py:
while True:
print('Please type your name.')
name = input('>')
if name == 'your name':
break
print('Thank you!')
第一行创建了一个无限循环;它是一个条件始终为True的while循环。(毕竟,True表达式始终评估为True值。)程序执行进入这个循环后,只有当执行到break语句时才会退出循环。(一个永远不会退出的无限循环是常见的编程错误。)
就像之前一样,这个程序会要求用户输入your name。然而,现在,当执行仍然在while循环内部时,一个if语句检查name是否等于'your name'。如果这个条件为True,则执行break语句,并将执行移动出循环以打印Thank you!。否则,包含break语句的if语句子句会被跳过,这使得执行到达while循环的末尾。此时,程序执行跳回到while语句的开始以重新检查条件。由于这个条件仅仅是True布尔值,执行会进入循环再次要求用户输入your name。见图 3-4 为此程序流程图。

图 3-4:包含无限循环的yourName2.py程序的流程图。注意,X 路径在逻辑上永远不会发生,因为循环条件始终为True。描述
运行yourName2.py,并输入与yourName.py相同的文本。重写的程序应该以与原始程序相同的方式响应。
continue 语句
与break语句一样,我们在循环中使用continue语句。当程序执行到达continue语句时,程序执行会立即跳回到循环的开始并重新评估循环的条件。(这也是当执行到达循环末尾时发生的情况。)
让我们使用continue编写一个请求用户输入用户名和密码的程序。将以下代码输入到一个新的文件编辑窗口中,并将程序保存为swordfish.py:
while True:
print('Who are you?')
name = input('>')
if name != 'Joe': # ❶
if name != 'Joe': # ❶
print('Hello, Joe. What is the password? (It is a fish.)')
if name != 'Joe': # ❶
if password == 'swordfish':
if name != 'Joe': # ❶
print('Access granted.') # ❺
如果用户输入除了Joe以外的任何名字❶,continue语句❷会导致程序执行跳回到循环的开始。当程序重新评估条件时,执行将始终进入循环,因为条件仅仅是值True。一旦用户通过了那个if语句,他们将被要求输入密码❸。如果输入的密码是swordfish,则执行break语句❹,并将执行跳出while循环以打印Access granted.❺否则,执行继续到while循环的末尾,然后跳回到循环的开始。见图 3-5 为此程序流程图。

图 3-5:swordfish.py程序的流程图。X 路径在逻辑上永远不会发生,因为循环条件始终为 True。描述
运行这个程序,给它一些输入。直到你声称自己是 Joe,程序不应该要求密码,一旦你输入正确的密码,它应该退出:
Who are you?
>I'm fine, thanks. Who are you?
Who are you?
>Joe
Hello, Joe. What is the password? (It is a fish.)
>Mary
Who are you?
>Joe
Hello, Joe. What is the password? (It is a fish.)
>swordfish
Access granted.
for循环和range()函数
while循环会在其条件为True时持续循环(这也是其名称的由来),但如果你只想执行一段代码特定次数,怎么办?你可以使用for循环语句和range()函数来实现。
在代码中,for语句看起来像for i in range(5):,并包括以下内容:
-
for关键字 -
变量名
-
in关键字 -
传递给
range()函数的最多三个整数 -
一个冒号
-
从下一行开始,一个缩进的代码块(称为
for子句或for块)
让我们创建一个新的程序,名为fiveTimes.py,以帮助你看到for循环的实际应用:
print('Hello!')
for i in range(5):
print('On this iteration, i is set to ' + str(i))
print('Goodbye!')
for循环子句中的代码运行了五次。第一次运行时,变量i被设置为0。子句中的print()调用将打印在这个迭代中,i 被设置为 0。当 Python 完成对for循环子句内部所有代码的迭代后,执行将回到循环的顶部,for语句将i增加 1。这就是为什么range(5)导致对子句进行五次迭代,i被设置为0,然后是1,然后是2,然后是3,最后是4。变量i将增加到但不包括传递给range()的整数。图 3-6 显示了*fiveTimes.py*程序的流程图。

图 3-6:fiveTimes.py程序的流程图。描述
这是程序的完整输出:
Hello!
On this iteration, i is set to 0
On this iteration, i is set to 1
On this iteration, i is set to 2
On this iteration, i is set to 3
On this iteration, i is set to 4
Goodbye!
你也可以在for循环中使用break和continue语句。continue语句将跳转到for循环计数器的下一个值,就像程序执行到达循环末尾并返回开始一样。实际上,你只能在while和for循环中使用continue和break语句。如果你在其他地方尝试使用这些语句,Python 会给你一个错误。
作为另一个for循环的例子,考虑一下关于数学家卡尔·弗里德里希·高斯的故事。当高斯还是个孩子的时候,一位老师想给全班一些忙碌的工作。老师告诉他们把从 0 到 100 的所有数字加起来。年轻的高斯想出了一个巧妙的办法,在几秒钟内就找到了答案,但你也可以用带有for循环的 Python 程序来做这个计算:
total = 0
for num in range(101):
total = total + num
print(total)
结果应该是 5,050。当程序首次启动时,total变量被设置为0。然后for循环执行total = total + num 101 次,每次num变量递增。当循环完成所有 101 次迭代后,从0到100的每个整数都将被添加到total中。此时,total将被打印到屏幕上。即使在最慢的计算机上,这个程序完成所需的时间也少于 1 秒。
(年轻的格奥尔基·康斯坦丁诺维奇·拉扎列夫·欧拉在几秒钟内就找到了解决问题的方法。有 50 对数字相加等于 101:1 + 100,2 + 99,3 + 98,以此类推,直到 50 + 51。由于 50 乘以 101 等于 5,050,所以从 0 到 100 的所有数字之和是 5,050。聪明的孩子!)
等价的while循环
实际上,你可以使用while循环来做与for循环相同的事情;for循环只是更简洁。让我们将*fiveTimes.py*重写为使用for循环的等价while循环:
print('Hello!')
i = 0
while i < 5:
print('On this iteration, i is set to ' + str(i))
i = i + 1
print('Goodbye!')
如果你运行这个程序,输出应该与使用for循环的*fiveTimes.py*程序相同。记住,for循环对于循环特定次数非常有用,而while循环则是在特定条件为真时循环。
range()函数的参数
一些函数可以通过逗号分隔的多个参数来调用,range()函数就是其中之一。这允许你将传递给range()的整数更改为任何整数序列,包括从非零数字开始:
for i in range(12, 16):
print(i)
第一个参数将是for循环变量的起始位置,第二个参数将是停止的数字,但不包括该数字:
12
13
14
15
range()函数也可以用三个参数调用。前两个参数将是起始值和停止值,第三个将是步长参数。步长是在每次迭代后变量增加的量:
for i in range(0, 10, 2):
print(i)
因此,调用range(0, 10, 2)将以两个间隔从零计数到八:
0
2
4
6
8
range()函数在为for循环生成数字序列方面非常灵活。例如(我从不为我的双关语道歉),你甚至可以使用负数作为步长参数,使for循环递减而不是递增:
for i in range(5, -1, -1):
print(i)
这个for循环将产生以下输出:
5
4
3
2
1
0
使用range(5, -1, -1)打印i的for循环应该打印从五到零的数字。
导入模块
所有 Python 程序都可以调用一组基本函数,称为内置函数,包括之前见过的print()、input()和len()函数。Python 还附带一组称为标准库的模块。每个模块都是一个包含相关函数组的 Python 程序,这些函数可以嵌入到你的程序中。例如,math模块包含数学相关的函数,random模块包含随机数相关的函数,等等。
在你能够使用模块中的函数之前,你必须使用import语句导入该模块。在代码中,import语句由以下内容组成:
-
import关键字 -
模块名称
-
可选地,更多模块名称,只要它们之间用逗号分隔
一旦导入了一个模块,你就可以使用该模块的所有酷炫功能。让我们尝试使用random模块,这将使我们能够访问random.randint()函数。
将以下代码输入到文件编辑器中,并将其保存为printRandom.py:
import random
for i in range(5):
print(random.randint(1, 10))
当你运行这个程序时,输出将类似于以下内容:
4
1
8
4
1
random.randint()函数调用会评估为两个整数之间的随机整数值。因为randint()在random模块中,所以必须在函数名前输入random.来告诉 Python 在random模块中查找此函数。
下面是一个导入四个不同模块的import语句示例:
import random, sys, os, math
现在我们可以使用这些四个模块中的任何函数。你将在本书的后面部分了解更多关于它们的信息。
import语句的另一种形式是由from关键字、模块名称、import关键字和一个星号(*)组成;例如,from random import *。使用这种形式的import语句,对random中函数的调用不需要random.前缀。然而,使用完整名称可以使代码更易读,因此最好使用import random形式的语句。
使用 sys.exit()提前结束程序
最后要介绍的控制流概念是如何终止或退出程序。如果程序执行到达指令的底部,程序总是会终止。然而,你可以通过调用sys.exit()函数在最后一条指令之前使程序终止。由于此函数在sys模块中,你必须在程序中使用它之前导入sys。
打开文件编辑器窗口,并输入以下代码,将其保存为exitExample.py:
import sys
while True:
print('Type exit to exit.')
response = input('>')
if response == 'exit':
sys.exit()
print('You typed ' + response + '.')
在你的代码编辑器中运行这个程序。这个程序包含一个无限循环,且内部没有break语句。这个程序唯一的结束方式是执行到sys.exit()调用。当response等于'exit'时,包含sys.exit()调用的那行代码会被执行。由于response变量是由input()函数设置的,用户必须输入exit才能停止程序。
短小精悍的程序:猜数字
我之前展示的示例对于介绍基本概念很有用,但现在你将看到你所学的一切如何在更完整的程序中结合起来。在本节中,我将向你展示一个简单的猜数字游戏。当你运行这个程序时,输出将类似于以下内容:
I am thinking of a number between 1 and 20.
Take a guess.
>10
Your guess is too low.
Take a guess.
>15
Your guess is too low.
Take a guess.
>17
Your guess is too high.
Take a guess.
>16
Good job! You got it in 4 guesses!
将以下源代码输入到文件编辑器中,并将其保存为guessTheNumber.py:
# This is a guess the number game.
import random
secret_number = random.randint(1, 20)
print('I am thinking of a number between 1 and 20.')
# Ask the player to guess 6 times.
for guesses_taken in range(1, 7):
print('Take a guess.')
guess = int(input('>'))
if guess < secret_number:
print('Your guess is too low.')
elif guess > secret_number:
print('Your guess is too high.')
else:
break # This condition is the correct guess!
if guess == secret_number:
print('Good job! You got it in ' + str(guesses_taken) + ' guesses!')
else:
print('Nope. The number was ' + str(secret_number))
让我们逐行查看这段代码,从顶部开始:
# This is a guess the number game.
import random
secret_number = random.randint(1, 20)
首先,代码顶部的注释解释了程序的功能。然后,程序导入random模块,以便可以使用random.randint()函数为用户生成一个猜测的数字。返回值,一个介于 1 到 20 之间的随机整数,被存储在变量secret_number中:
print('I am thinking of a number between 1 and 20.')
# Ask the player to guess 6 times.
for guesses_taken in range(1, 7):
print('Take a guess.')
guess = int(input('>'))
程序告诉玩家它已经想出了一个秘密数字,并会给玩家六次猜测的机会。代码允许玩家输入一个猜测,并在最多循环六次的for循环中检查这个猜测。循环中发生的第一件事是玩家输入一个猜测。因为input()返回一个字符串,所以它的返回值直接传递给int(),将字符串转换为整数值。这个值被存储在一个名为guess的变量中:
if guess < secret_number:
print('Your guess is too low.')
elif guess > secret_number:
print('Your guess is too high.')
这几行代码检查猜测是否小于或大于秘密数字。在两种情况下,都会在屏幕上打印出提示:
else:
break # This condition is the correct guess!
如果猜测既不高于也不低于秘密数字,那么它必须等于秘密数字——在这种情况下,你希望程序执行跳出for循环:
if guess == secret_number:
print('Good job! You got it in ' + str(guesses_taken) + ' guesses!')
else:
print('Nope. The number was ' + str(secret_number))
在for循环之后,之前的if-else语句检查玩家是否正确猜出了数字,然后向屏幕打印一条适当的消息。在两种情况下,程序都会显示一个包含整数值的变量(guesses_taken和secret_number)。由于它必须将这些整数值连接成字符串,因此将这些变量传递给str()函数,该函数返回这些整数的字符串形式。现在这些字符串可以用+运算符连接起来,最后传递给print()函数调用。
简短程序:剪刀、石头、布
让我们使用我们迄今为止学到的编程概念来创建一个简单的剪刀、石头、布游戏。输出将如下所示:
ROCK, PAPER, SCISSORS
0 Wins, 0 Losses, 0 Ties
Enter your move: (r)ock (p)aper (s)cissors or (q)uit
>p
PAPER versus...
PAPER
It is a tie!
0 Wins, 0 Losses, 1 Ties
Enter your move: (r)ock (p)aper (s)cissors or (q)uit
>s
SCISSORS versus...
PAPER
You win!
1 Wins, 0 Losses, 1 Ties
Enter your move: (r)ock (p)aper (s)cissors or (q)uit
>q
将以下源代码输入到文件编辑器中,并将文件保存为rpsGame.py:
import random, sys
print('ROCK, PAPER, SCISSORS')
# These variables keep track of the number of wins, losses, and ties.
wins = 0
losses = 0
ties = 0
while True: # The main game loop
print('%s Wins, %s Losses, %s Ties' % (wins, losses, ties))
while True: # The player input loop
print('Enter your move: (r)ock (p)aper (s)cissors or (q)uit')
player_move = input('>')
if player_move == 'q':
sys.exit() # Quit the program.
if player_move == 'r' or player_move == 'p' or player_move == 's':
break # Break out of the player input loop.
print('Type one of r, p, s, or q.')
# Display what the player chose:
if player_move == 'r':
print('ROCK versus...')
elif player_move == 'p':
print('PAPER versus...')
elif player_move == 's':
print('SCISSORS versus...')
# Display what the computer chose:
move_number = random.randint(1, 3)
if move_number == 1:
computer_move = 'r'
print('ROCK')
elif move_number == 2:
computer_move = 'p'
print('PAPER')
elif move_number == 3:
computer_move = 's'
print('SCISSORS')
# Display and record the win/loss/tie:
if player_move == computer_move:
print('It is a tie!')
ties = ties + 1
elif player_move == 'r' and computer_move == 's':
print('You win!')
wins = wins + 1
elif player_move == 'p' and computer_move == 'r':
print('You win!')
wins = wins + 1
elif player_move == 's' and computer_move == 'p':
print('You win!')
wins = wins + 1
elif player_move == 'r' and computer_move == 'p':
print('You lose!')
losses = losses + 1
elif player_move == 'p' and computer_move == 's':
print('You lose!')
losses = losses + 1
elif player_move == 's' and computer_move == 'r':
print('You lose!')
losses = losses + 1
让我们逐行查看这段代码,从顶部开始:
import random, sys
print('ROCK, PAPER, SCISSORS')
# These variables keep track of the number of wins, losses, and ties.
wins = 0
losses = 0
ties = 0
首先,我们导入random和sys模块,以便我们的程序可以调用random.randint()和sys.exit()函数。我们还设置了三个变量来跟踪玩家赢得、输掉和打平的次数:
while True: # The main game loop
print('%s Wins, %s Losses, %s Ties' % (wins, losses, ties))
while True: # The player input loop
print('Enter your move: (r)ock (p)aper (s)cissors or (q)uit')
player_move = input('>')
if player_move == 'q':
sys.exit() # Quit the program.
if player_move == 'r' or player_move == 'p' or player_move == 's':
break # Break out of the player input loop.
print('Type one of r, p, s, or q.')
这个程序在另一个while循环内部使用while循环。第一个循环是主游戏循环,每次迭代都会在这个循环中玩一局剪刀、石头、布。第二个循环从玩家那里获取输入,并持续循环,直到玩家输入了r、p、s或q作为他们的移动。r、p和s分别代表石头、布和剪刀,而q表示玩家打算退出。在这种情况下,调用sys.exit()并退出程序。如果玩家输入了r、p或s,则执行跳出循环。否则,程序会提醒玩家输入r、p、s或q,并回到循环的开始:
# Display what the player chose:
if player_move == 'r':
print('ROCK versus...')
elif player_move == 'p':
print('PAPER versus...')
elif player_move == 's':
print('SCISSORS versus...')
玩家的移动显示在屏幕上:
# Display what the computer chose:
move_number = random.randint(1, 3)
if move_number == 1:
computer_move = 'r'
print('ROCK')
elif move_number == 2:
computer_move = 'p'
print('PAPER')
elif move_number == 3:
computer_move = 's'
print('SCISSORS')
接下来,程序随机选择电脑的移动。因为random.randint()总是会返回一个随机数,所以代码将返回的1、2或3整数值存储在一个名为move_number的变量中。然后程序根据move_number中的整数在computer_move中存储一个'r'、'p'或's'字符串,并显示电脑的移动:
# Display and record the win/loss/tie:
if player_move == computer_move:
print('It is a tie!')
ties = ties + 1
elif player_move == 'r' and computer_move == 's':
print('You win!')
wins = wins + 1
elif player_move == 'p' and computer_move == 'r':
print('You win!')
wins = wins + 1
elif player_move == 's' and computer_move == 'p':
print('You win!')
wins = wins + 1
elif player_move == 'r' and computer_move == 'p':
print('You lose!')
losses = losses + 1
elif player_move == 'p' and computer_move == 's':
print('You lose!')
losses = losses + 1
elif player_move == 's' and computer_move == 'r':
print('You lose!')
losses = losses + 1
最后,程序比较player_move和computer_move中的字符串,并在屏幕上显示结果。它还会适当地增加wins、losses或ties变量。一旦执行到达末尾,它就会跳回到主程序循环的开始,开始另一场比赛。
如果你喜欢这些猜数字和剪刀石头布游戏,你可以在《大型小型 Python 项目集》(No Starch Press,2021 年)中找到其他简单 Python 程序的源代码。
概述
循环允许你的程序在某个条件评估为True时重复执行代码。如果需要退出循环或跳回到循环的开始,break和continue语句非常有用。
这些流程控制语句将使你能够编写更智能的程序。你还可以通过编写自己的函数来使用另一种类型的流程控制,这是下一章的主题。
实践问题
1. 如果你的 Python 程序陷入无限循环,你可以按哪些键?
2. break和continue之间的区别是什么?
3. 在for循环中,range(10)、range(0, 10)和range(0, 10, 1)有什么区别?
4. 编写一个短程序,使用for循环打印数字1到10。然后,编写一个等效程序,使用while循环打印数字1到10。
5. 如果你在名为spam的模块中有一个名为bacon()的函数,在导入spam后如何调用它?
while 循环语句
你可以使用while语句使代码块重复执行。while子句中的代码会在条件为True时执行。在代码中,while语句始终由以下内容组成:
-
while关键字 -
一个条件(即评估为
True或False的表达式) -
一个冒号
-
从下一行开始,一个缩进的代码块(称为
while子句或while块)
你可以看到,while语句看起来与if语句相似。区别在于它们的行为。在if子句的末尾,程序执行会在if语句之后继续。但在while子句的末尾,程序执行会跳回到while语句的开始。while子句通常被称为while 循环或简称为循环。
让我们看看使用相同条件并基于该条件执行相同操作的if语句和while循环。以下是使用if语句的代码:
spam = 0
if spam < 5:
print('Hello, world.')
spam = spam + 1
这是使用while语句的代码:
spam = 0
while spam < 5:
print('Hello, world.')
spam = spam + 1
这些语句类似;if 和 while 都检查 spam 的值,如果它小于 5,则打印一条消息。但当你运行这两个代码片段时,每个片段都会发生非常不同的情况。对于 if 语句,输出只是 "Hello, world."。但对于 while 语句,它是重复五次的 "Hello, world."!查看这两个代码片段的流程图,图 3-1 和 3-2,以了解为什么会发生这种情况。

图 3-1:if 语句代码流程图说明
带有 if 语句的代码检查条件,如果该条件为真,则只打印一次 "Hello, world."。另一方面,带有 while 循环的代码将打印五次。循环在打印五次后停止,因为在每次循环迭代结束时,spam 中的整数增加一,这意味着循环将在 spam < 5 为 False 之前执行五次。

图 3-2:while 语句代码流程图说明
在 while 循环中,条件始终在每个 迭代(即每次循环执行时)的开始处进行检查。如果条件为 True,则执行子句,之后再次检查条件。第一次发现条件为 False 时,跳过 while 子句。
一个烦人的 while 循环
这里有一个小型的示例程序,它将不断地要求你输入,实际上就是你的名字。选择 文件新建 以打开一个新的文件编辑器窗口,输入以下代码,并将文件保存为 yourName.py:
name = ''
while name != 'your name':
print('Please type your name.')
name = input('>')
print('Thank you!')
首先,程序将 name 变量设置为空字符串。这样做是为了使 name != 'your name' 条件评估为 True,程序执行将进入 while 循环的子句。
这个子句中的代码要求用户输入他们的名字,并将其分配给 name 变量。由于这是块的最后一行,执行会回到 while 循环的开始并重新评估条件。如果 name 中的值不等于字符串 'your name',则条件为 True,执行再次进入 while 子句。
但一旦用户确实输入了 your name,while 循环的条件将变为 'your name' != 'your name',这评估为 False。现在,程序执行不会重新进入 while 循环的子句,Python 会跳过它并继续运行程序的其余部分。图 3-3 显示了 yourName.py 程序的流程图。

图 3-3:yourName.py 程序流程图说明
现在,让我们看看 yourName.py 的实际效果。按 F5 运行它,在你给出程序所需的内容之前,输入几次除 your name 之外的内容:
Please type your name.
>Al
Please type your name.
>Albert
Please type your name.
>%#@#%*(^&!!!
Please type your name.
>your name
Thank you!
如果你从未输入 your name,那么 while 循环的条件永远不会变为 False,程序将永远不断地询问你问题。在这里,input() 调用允许用户输入正确的字符串,使程序继续运行。在其他程序中,条件可能实际上永远不会改变,这可能会成为一个问题。让我们看看如何跳出 while 循环。
break 语句
有一种方法可以提前使程序执行跳出 while 循环的条件。如果执行到达一个 break 语句,它将立即退出 while 循环的条件。在代码中,break 语句仅包含 break 关键字。
下面是一个程序,它执行与之前的 yourName.py 程序相同的功能,但使用 break 语句来跳出循环。输入以下代码,并将文件保存为 yourName2.py:
while True:
print('Please type your name.')
name = input('>')
if name == 'your name':
break
print('Thank you!')
第一行创建了一个 无限循环;它是一个条件始终为 True 的 while 循环。(毕竟,True 表达式始终评估为 True 的值。)程序执行进入这个循环后,只有当执行到 break 语句时才会退出循环。(永远不会退出的无限循环是常见的编程错误。)
就像之前一样,这个程序要求用户输入 your name。现在,然而,当执行仍在 while 循环内部时,一个 if 语句检查 name 是否等于 'your name'。如果这个条件为 True,则执行 break 语句,执行将跳出循环到 print('Thank you!')。否则,包含 break 语句的 if 语句子句将被跳过,执行将到达 while 循环的末尾。此时,程序执行将跳回到 while 语句的开始以重新检查条件。由于这个条件仅仅是 True 布尔值,执行将进入循环再次要求用户输入 your name。参见图 3-4 了解此程序的流程图。

图 3-4:yourName2.py 程序的流程图,包含带有“True”和“False”标签的分支路径。注意,X 路径在逻辑上永远不会发生,因为循环条件始终为 True。描述
运行 yourName2.py,并输入与 yourName.py 中输入的相同文本。重写的程序应该以与原始程序相同的方式响应。
continue 语句
与 break 语句类似,我们在循环中使用 continue 语句。当程序执行到达一个 continue 语句时,程序执行会立即跳回到循环的开始并重新评估循环的条件。(这也是当执行到达循环末尾时发生的情况。)
让我们使用 continue 来编写一个询问用户姓名和密码的程序。将以下代码输入到一个新的文件编辑窗口中,并将程序保存为 swordfish.py:
while True:
print('Who are you?')
name = input('>')
if name != 'Joe': # ❶
if name != 'Joe': # ❶
print('Hello, Joe. What is the password? (It is a fish.)')
if name != 'Joe': # ❶
if password == 'swordfish':
if name != 'Joe': # ❶
print('Access granted.') # ❺
如果用户输入了除 Joe ❶ 之外的名字,continue 语句 ❷ 将导致程序执行跳回循环的开始。当程序重新评估条件时,由于条件仅仅是值 True,执行将始终进入循环。一旦用户通过了那个 if 语句,他们将被要求输入密码 ❸。如果输入的密码是 swordfish,将执行 break 语句 ❹,执行将跳出 while 循环并打印 Access granted. ❺ 否则,执行将继续到 while 循环的末尾,然后跳回循环的开始。请参阅图 3-5 了解此程序的流程图。

图 3-5:swordfish.py 程序的流程图。X 路径在逻辑上永远不会发生,因为循环条件始终为 True。描述
运行这个程序并给它一些输入。直到你声称自己是 Joe,程序才不应该询问密码,一旦你输入正确的密码,程序应该退出:
Who are you?
>I'm fine, thanks. Who are you?
Who are you?
>Joe
Hello, Joe. What is the password? (It is a fish.)
>Mary
Who are you?
>Joe
Hello, Joe. What is the password? (It is a fish.)
>swordfish
Access granted.
一个烦人的 while 循环
这里有一个小型的示例程序,它会不断地要求你输入,字面意义上就是你的名字。选择文件新建来打开一个新的文件编辑窗口,输入以下代码,并将文件保存为 yourName.py:
name = ''
while name != 'your name':
print('Please type your name.')
name = input('>')
print('Thank you!')
首先,程序将 name 变量设置为空字符串。这样做是为了使 name != 'your name' 条件评估为 True,程序执行将进入 while 循环的子句。
此子句中的代码要求用户输入他们的名字,并将其分配给 name 变量。由于这是块的最后一行,执行将返回到 while 循环的开始并重新评估条件。如果 name 中的值不等于字符串 'your name',则条件为 True,执行将再次进入 while 子句。
但是一旦用户确实输入了 your name,while 循环的条件将变为 'your name' != 'your name',这评估为 False。现在,程序执行不会重新进入 while 循环的子句,Python 会跳过它并继续运行程序的其余部分。图 3-3 显示了 yourName.py 程序的流程图。

图 3-3:yourName.py 程序的流程图。描述
现在让我们看看 yourName.py 的实际运行情况。按 F5 运行它,在你给出程序所需的内容之前,输入几次除 your name 之外的内容:
Please type your name.
>Al
Please type your name.
>Albert
Please type your name.
>%#@#%*(^&!!!
Please type your name.
>your name
Thank you!
如果你从未输入 your name,那么 while 循环的条件将永远不会变为 False,程序将永远不断地询问你问题。在这里,input() 调用允许用户输入正确的字符串以使程序继续。在其他程序中,条件可能实际上永远不会改变,这可能会成为一个问题。让我们看看如何跳出 while 循环。
break 语句
有一种快捷方式可以让程序执行在 while 循环的子句中提前退出。如果执行到达 break 语句,它将立即退出 while 循环的子句。在代码中,break 语句仅包含 break 关键字。
这里有一个程序,它与之前的 yourName.py 程序做相同的事情,但使用 break 语句来跳出循环。输入以下代码,并将文件保存为 yourName2.py:
while True:
print('Please type your name.')
name = input('>')
if name == 'your name':
break
print('Thank you!')
第一行创建了一个 无限循环;它是一个条件始终为 True 的 while 循环。(毕竟,True 表达式始终评估为 True 的值。)程序执行进入此循环后,只有在执行 break 语句时才会退出循环。(永远不会退出的无限循环是常见的编程错误。)
就像之前一样,这个程序会要求用户输入 your name。现在,然而,当执行仍在 while 循环内部时,一个 if 语句检查 name 是否等于 'your name'。如果这个条件为 True,则执行 break 语句,并将执行移动出循环以 print('Thank you!')。否则,包含 break 语句的 if 语句子句将被跳过,这使得执行到达 while 循环的末尾。此时,程序执行跳回到 while 语句的开始以重新检查条件。由于这个条件仅仅是 True 布尔值,执行将进入循环以再次要求用户输入 your name。参见图 3-4 了解此程序的流程图。

图 3-4:yourName2.py 程序的流程图,包含无限循环。注意,X 路径在逻辑上永远不会发生,因为循环条件始终为 True。描述
运行 yourName2.py,并输入与 yourName.py 中相同的文本。重写的程序应该以与原始程序相同的方式响应。
continue 语句
与 break 语句一样,我们在循环中使用 continue 语句。当程序执行到达 continue 语句时,程序执行将立即跳回到循环的开始并重新评估循环的条件。(这也是当执行到达循环末尾时发生的情况。)
让我们使用 continue 来编写一个请求用户输入用户名和密码的程序。将以下代码输入到一个新的文件编辑器窗口中,并将程序保存为 swordfish.py:
while True:
print('Who are you?')
name = input('>')
if name != 'Joe': # ❶
if name != 'Joe': # ❶
print('Hello, Joe. What is the password? (It is a fish.)')
if name != 'Joe': # ❶
if password == 'swordfish':
if name != 'Joe': # ❶
print('Access granted.') # ❺
如果用户输入除了 Joe ❶ 之外的名字,continue 语句 ❷ 将导致程序执行跳回循环的开始。当程序重新评估条件时,执行将始终进入循环,因为条件仅仅是值 True。一旦用户通过了那个 if 语句,他们将被要求输入密码 ❸。如果输入的密码是 swordfish,将执行 break 语句 ❹,并且执行将跳出 while 循环并打印 Access granted. ❺ 否则,执行将继续到 while 循环的末尾,然后跳回循环的开始。请参阅图 3-5 了解此程序的流程图。

图 3-5:swordfish.py 程序的流程图。X 路径在逻辑上永远不会发生,因为循环条件始终为 True。描述
运行此程序并给它一些输入。直到你声称自己是 Joe,程序不应该要求密码,一旦你输入正确的密码,它应该退出:
Who are you?
>I'm fine, thanks. Who are you?
Who are you?
>Joe
Hello, Joe. What is the password? (It is a fish.)
>Mary
Who are you?
>Joe
Hello, Joe. What is the password? (It is a fish.)
>swordfish
Access granted.
for 循环和 range() 函数
while 循环在条件为 True(这也是其名称的原因)时持续循环,但如果你只想执行一段代码特定次数,你可以使用 for 循环语句和 range() 函数来实现。
在代码中,for 语句看起来像 for i in range(5): 并包括以下内容:
-
for关键字 -
变量名
-
in关键字 -
向
range()函数传递最多三个整数 -
一个冒号
-
从下一行开始,一个缩进的代码块(称为
for子句或for块)
让我们创建一个新的程序,命名为 fiveTimes.py,以帮助您看到 for 循环的实际应用:
print('Hello!')
for i in range(5):
print('On this iteration, i is set to ' + str(i))
print('Goodbye!')
for 循环子句中的代码运行了五次。第一次运行时,变量 i 被设置为 0。子句中的 print() 调用将打印 在这个迭代中,i 被设置为 0。当 Python 完成对 for 循环子句内部所有代码的迭代后,执行将回到循环的顶部,for 语句将 i 增加 1。这就是为什么 range(5) 导致子句迭代五次,i 被设置为 0,然后 1,然后 2,然后 3,然后 4。变量 i 将增加到但不包括传递给 range() 的整数。图 3-6 显示了 fiveTimes.py 程序的流程图。

图 3-6:fiveTimes.py 程序的流程图描述
这是程序的完整输出:
Hello!
On this iteration, i is set to 0
On this iteration, i is set to 1
On this iteration, i is set to 2
On this iteration, i is set to 3
On this iteration, i is set to 4
Goodbye!
你也可以在 for 循环中使用 break 和 continue 语句。continue 语句将继续到 for 循环计数器的下一个值,就像程序执行到达循环的末尾并返回开始一样。实际上,你只能在 while 和 for 循环中使用 continue 和 break 语句。如果你在其他地方尝试使用这些语句,Python 将会给你一个错误。
作为另一个 for 循环的例子,考虑一下关于数学家卡尔·弗里德里希·高斯的故事。当高斯还是个孩子的时候,一位老师想要给学生一些忙碌的工作。老师告诉他们把从 0 到 100 的所有数字加起来。年轻的高斯想出了一个巧妙的办法,在几秒钟内就找到了答案,但你也可以用带有 for 循环的 Python 程序来做这个计算:
total = 0
for num in range(101):
total = total + num
print(total)
结果应该是 5,050。当程序第一次启动时,total 变量被设置为 0。然后 for 循环执行 total = total + num 101 次,每次都有一个递增的 num 变量。当循环完成所有 101 次迭代后,从 0 到 100 的每个整数都将被加到 total 上。此时,total 将被打印到屏幕上。即使在最慢的计算机上,这个程序也只需不到一秒钟就能完成。
(年轻的高斯找到了一种在几秒钟内解决问题的方法。有 50 对数字相加等于 101:1 + 100,2 + 99,3 + 98,以此类推,直到 50 + 51。由于 50 乘以 101 等于 5,050,所以从 0 到 100 的所有数字之和是 5,050。真是个聪明的孩子!)
等价的 while 循环
你实际上可以用 while 循环来做和 for 循环一样的事情;for 循环只是更简洁。让我们将 fiveTimes.py 重写为使用 while 循环的 for 循环等价形式:
print('Hello!')
i = 0
while i < 5:
print('On this iteration, i is set to ' + str(i))
i = i + 1
print('Goodbye!')
如果你运行这个程序,输出应该和使用 for 循环的 fiveTimes.py 程序相同。记住,for 循环对于循环特定次数非常有用,而 while 循环则是在特定条件为真时循环。
range() 的参数
一些函数可以用逗号分隔的多个参数调用,range() 就是其中之一。这让你可以改变传递给 range() 的整数,以遵循任何整数序列,包括从非零数字开始:
for i in range(12, 16):
print(i)
第一个参数将是 for 循环变量的起始位置,第二个参数将是停止的数字,但不包括该数字:
12
13
14
15
range() 函数也可以用三个参数调用。前两个参数将是起始值和停止值,第三个将是 步长参数。步长是在每次迭代后变量增加的量:
for i in range(0, 10, 2):
print(i)
因此,调用 range(0, 10, 2) 将以两个间隔从零计数到八:
0
2
4
6
8
range() 函数在为 for 循环生成数字序列方面非常灵活。例如(我从不为我的双关语道歉),你甚至可以使用负数作为步长参数,使 for 循环倒计时而不是递增:
for i in range(5, -1, -1):
print(i)
这个 for 循环将产生以下输出:
5
4
3
2
1
0
使用 range(5, -1, -1) 打印 i 的 for 循环应该打印从五到零的数字。
等价的 while 循环
你实际上可以使用 while 循环来做与 for 循环相同的事情;for 循环只是更简洁。让我们将 fiveTimes.py 重写为使用 while 循环的 for 循环等价物:
print('Hello!')
i = 0
while i < 5:
print('On this iteration, i is set to ' + str(i))
i = i + 1
print('Goodbye!')
如果你运行这个程序,输出应该看起来与使用 for 循环的 fiveTimes.py 程序相同。记住,for 循环对于循环特定次数非常有用,而 while 循环对于在特定条件为真时循环非常有用。
range() 的参数
一些函数可以用逗号分隔的多个参数调用,range() 就是其中之一。这让你可以将传递给 range() 的整数更改为遵循任何整数序列,包括从非零数字开始:
for i in range(12, 16):
print(i)
第一个参数将是 for 循环变量的起始位置,第二个参数将是停止的数字,但不包括该数字:
12
13
14
15
range() 函数也可以用三个参数调用。前两个参数将是起始值和停止值,第三个将是 步长参数。步长是在每次迭代后变量增加的量:
for i in range(0, 10, 2):
print(i)
因此,调用 range(0, 10, 2) 将以两个间隔从零计数到八:
0
2
4
6
8
range() 函数在为 for 循环生成数字序列方面非常灵活。例如(我从不为我的双关语道歉),你甚至可以使用负数作为步长参数,使 for 循环倒计时而不是递增:
for i in range(5, -1, -1):
print(i)
这个 for 循环将产生以下输出:
5
4
3
2
1
0
使用 range(5, -1, -1) 打印 i 的 for 循环应该打印从五到零的数字。
导入模块
所有 Python 程序都可以调用一组基本函数,称为 内置函数,包括你之前见过的 print()、input() 和 len() 函数。Python 还附带一组称为 标准库 的模块。每个模块都是一个包含相关函数组的 Python 程序,这些函数可以嵌入到你的程序中。例如,math 模块包含数学相关的函数,random 模块包含随机数相关的函数,等等。
在你可以使用模块中的函数之前,你必须使用 import 语句导入该模块。在代码中,import 语句由以下内容组成:
-
import关键字 -
模块的名称
-
可选地,更多的模块名称,只要它们用逗号分隔
一旦你导入了一个模块,你就可以使用该模块的所有酷炫功能。让我们用 random 模块试一试,这将使我们能够访问 random.randint() 函数。
将此代码输入到文件编辑器中,并将其保存为 printRandom.py:
import random
for i in range(5):
print(random.randint(1, 10))
当你运行此程序时,输出将类似于以下内容:
4
1
8
4
1
random.randint() 函数调用将评估为介于你传递给它的两个整数之间的随机整数值。因为 randint() 在 random 模块中,所以你必须首先在函数名前输入 random. 来告诉 Python 在 random 模块内查找此函数。
下面是一个导入语句的例子,它导入了四个不同的模块:
import random, sys, os, math
现在,我们可以使用这些四个模块中的任何函数。你将在本书的后面部分了解更多关于它们的信息。
import 语句的另一种形式由 from 关键字、模块名称、import 关键字和一个星号 (*) 组成;例如,from random import *。使用这种形式的 import 语句,对 random 中函数的调用不需要 random. 前缀。然而,使用完整名称可以使代码更易读,因此最好使用 import random 的语句形式。
使用 sys.exit() 提前结束程序
最后要介绍的控制流概念是如何 终止 或退出程序。如果程序执行到达指令的底部,程序总是会终止。然而,你可以通过调用 sys.exit() 函数在最后一条指令之前使程序终止。由于此函数在 sys 模块中,你必须在使用它之前导入 sys。
打开文件编辑器窗口,输入以下代码,并将其保存为 exitExample.py:
import sys
while True:
print('Type exit to exit.')
response = input('>')
if response == 'exit':
sys.exit()
print('You typed ' + response + '.')
在你的代码编辑器中运行此程序。此程序有一个没有 break 语句的无限循环。此程序结束的唯一方法是执行到 sys.exit() 调用。当 response 等于 'exit' 时,包含 sys.exit() 调用的行将被执行。由于 response 变量由 input() 函数设置,用户必须输入 exit 才能停止程序。
简短程序:猜数字
我之前展示的例子对于介绍基本概念很有用,但现在你将看到你所学的一切如何在更完整的程序中结合起来。在本节中,我将向你展示一个简单的猜数字游戏。当你运行此程序时,输出将类似于以下内容:
I am thinking of a number between 1 and 20.
Take a guess.
>10
Your guess is too low.
Take a guess.
>15
Your guess is too low.
Take a guess.
>17
Your guess is too high.
Take a guess.
>16
Good job! You got it in 4 guesses!
将以下源代码输入到文件编辑器中,并将文件保存为 guessTheNumber.py:
# This is a guess the number game.
import random
secret_number = random.randint(1, 20)
print('I am thinking of a number between 1 and 20.')
# Ask the player to guess 6 times.
for guesses_taken in range(1, 7):
print('Take a guess.')
guess = int(input('>'))
if guess < secret_number:
print('Your guess is too low.')
elif guess > secret_number:
print('Your guess is too high.')
else:
break # This condition is the correct guess!
if guess == secret_number:
print('Good job! You got it in ' + str(guesses_taken) + ' guesses!')
else:
print('Nope. The number was ' + str(secret_number))
让我们逐行查看此代码,从顶部开始:
# This is a guess the number game.
import random
secret_number = random.randint(1, 20)
首先,代码顶部的注释解释了程序的功能。然后,程序导入 random 模块,以便可以使用 random.randint() 函数为用户生成一个数字。返回值,一个介于 1 和 20 之间的随机整数,存储在变量 secret_number 中:
print('I am thinking of a number between 1 and 20.')
# Ask the player to guess 6 times.
for guesses_taken in range(1, 7):
print('Take a guess.')
guess = int(input('>'))
程序告诉玩家它已经想出了一个秘密数字,并会给玩家六次猜测的机会。代码允许玩家输入一个猜测,并在最多循环六次的for循环中检查这个猜测。循环中发生的第一件事是玩家输入一个猜测。因为input()返回一个字符串,所以它的返回值直接传递给int(),将字符串转换为整数值。这个值被存储在一个名为guess的变量中:
if guess < secret_number:
print('Your guess is too low.')
elif guess > secret_number:
print('Your guess is too high.')
这几行代码检查猜测是否小于或大于秘密数字。在任一情况下,都会在屏幕上打印一条提示:
else:
break # This condition is the correct guess!
如果猜测既不高于也不低于秘密数字,那么它必须等于秘密数字——在这种情况下,你希望程序执行跳出for循环:
if guess == secret_number:
print('Good job! You got it in ' + str(guesses_taken) + ' guesses!')
else:
print('Nope. The number was ' + str(secret_number))
在for循环之后,之前的if-else语句检查玩家是否正确猜出了数字,然后向屏幕打印一条适当的消息。在两种情况下,程序都会显示一个包含整数值的变量(guesses_taken和secret_number)。由于它必须将这些整数值连接成字符串,因此将这些变量传递给str()函数,该函数返回这些整数的字符串形式。现在这些字符串可以用+运算符连接起来,最后再传递给print()函数调用。
简短程序:剪刀石头布
让我们使用我们迄今为止学到的编程概念来创建一个简单的剪刀石头布游戏。输出将如下所示:
ROCK, PAPER, SCISSORS
0 Wins, 0 Losses, 0 Ties
Enter your move: (r)ock (p)aper (s)cissors or (q)uit
>p
PAPER versus...
PAPER
It is a tie!
0 Wins, 0 Losses, 1 Ties
Enter your move: (r)ock (p)aper (s)cissors or (q)uit
>s
SCISSORS versus...
PAPER
You win!
1 Wins, 0 Losses, 1 Ties
Enter your move: (r)ock (p)aper (s)cissors or (q)uit
>q
将以下源代码输入到文件编辑器中,并将文件保存为rpsGame.py:
import random, sys
print('ROCK, PAPER, SCISSORS')
# These variables keep track of the number of wins, losses, and ties.
wins = 0
losses = 0
ties = 0
while True: # The main game loop
print('%s Wins, %s Losses, %s Ties' % (wins, losses, ties))
while True: # The player input loop
print('Enter your move: (r)ock (p)aper (s)cissors or (q)uit')
player_move = input('>')
if player_move == 'q':
sys.exit() # Quit the program.
if player_move == 'r' or player_move == 'p' or player_move == 's':
break # Break out of the player input loop.
print('Type one of r, p, s, or q.')
# Display what the player chose:
if player_move == 'r':
print('ROCK versus...')
elif player_move == 'p':
print('PAPER versus...')
elif player_move == 's':
print('SCISSORS versus...')
# Display what the computer chose:
move_number = random.randint(1, 3)
if move_number == 1:
computer_move = 'r'
print('ROCK')
elif move_number == 2:
computer_move = 'p'
print('PAPER')
elif move_number == 3:
computer_move = 's'
print('SCISSORS')
# Display and record the win/loss/tie:
if player_move == computer_move:
print('It is a tie!')
ties = ties + 1
elif player_move == 'r' and computer_move == 's':
print('You win!')
wins = wins + 1
elif player_move == 'p' and computer_move == 'r':
print('You win!')
wins = wins + 1
elif player_move == 's' and computer_move == 'p':
print('You win!')
wins = wins + 1
elif player_move == 'r' and computer_move == 'p':
print('You lose!')
losses = losses + 1
elif player_move == 'p' and computer_move == 's':
print('You lose!')
losses = losses + 1
elif player_move == 's' and computer_move == 'r':
print('You lose!')
losses = losses + 1
让我们逐行查看这段代码,从顶部开始:
import random, sys
print('ROCK, PAPER, SCISSORS')
# These variables keep track of the number of wins, losses, and ties.
wins = 0
losses = 0
ties = 0
首先,我们导入random和sys模块,以便我们的程序可以调用random.randint()和sys.exit()函数。我们还设置了三个变量来跟踪玩家赢得、输掉和打平的次数:
while True: # The main game loop
print('%s Wins, %s Losses, %s Ties' % (wins, losses, ties))
while True: # The player input loop
print('Enter your move: (r)ock (p)aper (s)cissors or (q)uit')
player_move = input('>')
if player_move == 'q':
sys.exit() # Quit the program.
if player_move == 'r' or player_move == 'p' or player_move == 's':
break # Break out of the player input loop.
print('Type one of r, p, s, or q.')
这个程序在另一个while循环中使用了while循环。第一个循环是主游戏循环,每次迭代都会在这个循环中玩一局剪刀石头布。第二个循环从玩家那里获取输入,并持续循环,直到玩家输入了r、p、s或q作为他们的动作。r、p和s分别代表石头、布和剪刀,而q表示玩家打算退出。在这种情况下,会调用sys.exit()并退出程序。如果玩家输入了r、p或s,则执行将跳出循环。否则,程序会提醒玩家输入r、p、s或q,并回到循环的开始:
# Display what the player chose:
if player_move == 'r':
print('ROCK versus...')
elif player_move == 'p':
print('PAPER versus...')
elif player_move == 's':
print('SCISSORS versus...')
玩家的动作显示在屏幕上:
# Display what the computer chose:
move_number = random.randint(1, 3)
if move_number == 1:
computer_move = 'r'
print('ROCK')
elif move_number == 2:
computer_move = 'p'
print('PAPER')
elif move_number == 3:
computer_move = 's'
print('SCISSORS')
接下来,程序随机选择电脑的走法。因为 random.randint() 总是返回一个随机数,所以代码将返回的 1、2 或 3 整数值存储在一个名为 move_number 的变量中。然后程序根据 move_number 中的整数在 computer_move 中存储一个 'r'、'p' 或 's' 字符串,并显示电脑的走法:
# Display and record the win/loss/tie:
if player_move == computer_move:
print('It is a tie!')
ties = ties + 1
elif player_move == 'r' and computer_move == 's':
print('You win!')
wins = wins + 1
elif player_move == 'p' and computer_move == 'r':
print('You win!')
wins = wins + 1
elif player_move == 's' and computer_move == 'p':
print('You win!')
wins = wins + 1
elif player_move == 'r' and computer_move == 'p':
print('You lose!')
losses = losses + 1
elif player_move == 'p' and computer_move == 's':
print('You lose!')
losses = losses + 1
elif player_move == 's' and computer_move == 'r':
print('You lose!')
losses = losses + 1
最后,程序比较 player_move 和 computer_move 中的字符串,并在屏幕上显示结果。它还会适当地增加 wins、losses 或 ties 变量。一旦执行到达末尾,它就会跳回到主程序循环的开始,开始另一场比赛。
如果你喜欢这些猜数字和剪刀石头布游戏,你可以在 《Python 小项目大全书》(No Starch Press,2021 年)中找到其他简单 Python 程序的源代码。
摘要
循环允许你的程序在某个条件评估为 True 时重复执行代码。如果需要退出循环或跳回到循环的开始,break 和 continue 语句非常有用。
这些流程控制语句将使你能够编写更智能的程序。你还可以通过编写自己的函数来使用另一种类型的流程控制,这是下一章的主题。
实践问题
-
如果你的 Python 程序陷入无限循环,你可以按哪些键?
-
break和continue之间的区别是什么? -
在
for循环中,range(10)、range(0, 10)和range(0, 10, 1)有什么区别? -
编写一个使用
for循环打印数字1到10的简短程序。然后,编写一个等效程序,使用while循环打印数字1到10。 -
如果你在一个名为
spam的模块中有一个名为bacon()的函数,如何在导入spam后调用它?
4 函数

一个 函数 就像程序内部的迷你程序。Python 提供了几个内置函数,例如来自前几章的 print()、input() 和 len() 函数,但你也可以编写自己的函数。在本章中,你将创建函数,探索用于确定程序中函数运行顺序的调用栈,并了解函数内外变量的作用域。
创建函数
为了更好地理解函数是如何工作的,让我们创建一个函数。将以下程序输入到文件编辑器中,并保存为 helloFunc.py:
def hello():
# Prints three greetings
print('Good morning!')
print('Good afternoon!')
print('Good evening!')
hello()
hello()
print('ONE MORE TIME!')
hello()
第一行是一个 def 语句,它定义了一个名为 hello() 的函数。def 语句之后的代码块是函数的主体。这段代码在函数被调用时执行,而不是在函数首次定义时执行。
函数定义之后的 hello() 行是函数调用。在代码中,函数调用只是函数的名称后跟括号,括号之间可能有一些参数。当程序执行到达这些调用时,它将跳转到函数中的第一行并开始执行那里的代码。当它到达函数的末尾时,执行将返回到调用函数的行,并继续像以前一样通过代码。
因为这个程序调用了 hello() 三次,所以 hello() 函数中的代码执行了三次。当你运行这个程序时,输出看起来像这样:
Good morning!
Good afternoon!
Good evening!
Good morning!
Good afternoon!
Good evening!
ONE MORE TIME!
Good morning!
Good afternoon!
Good evening!
函数的主要目的是将多次执行的代码分组。如果没有定义函数,每次你想运行这段代码时,你都必须复制并粘贴它,程序看起来会是这样:
print('Good morning!')
print('Good afternoon!')
print('Good evening!')
print('Good morning!')
print('Good afternoon!')
print('Good evening!')
print('ONE MORE TIME!')
print('Good morning!')
print('Good afternoon!')
print(' Good evening!')
通常,你总是想避免代码重复,因为如果你决定更新代码(例如,因为你发现了一个需要修复的错误),你将不得不记住在每个复制了代码的地方更改代码。
随着你编程经验的积累,你经常会发现自己正在进行 去重,这意味着移除复制粘贴的代码。去重可以使你的程序更短,更容易阅读,也更容易更新。
参数和参数
当你调用 print() 或 len() 函数时,你通过在括号中输入它们来传递值,这些值被称为 参数。你也可以定义自己的函数,这些函数接受参数。将以下示例输入到文件编辑器中,并保存为 helloFunc2.py:
def say_hello_to(name): # ❶
# Prints three greetings to the name provided
print('Good morning, ' + name) # ❷
print('Good afternoon, ' + name)
print('Good evening, ' + name)
say_hello_to('Alice') # ❸
say_hello_to('Bob')
当你运行这个程序时,输出看起来像这样:
Good morning, Alice
Good afternoon, Alice
Good evening, Alice
Good morning, Bob
Good afternoon, Bob
Good evening, Bob
say_hello_to() 函数的定义中有一个名为 name 的参数 ❶。参数 是包含参数的变量。当函数用参数调用时,这些参数被存储在参数中。当 say_hello_to() 函数第一次被调用时,它传递了参数 'Alice' ❸。程序执行进入函数,参数 name 被自动设置为 'Alice',然后由 print() 语句打印出来。如果你需要你的函数根据传递给函数调用的值执行稍微不同的指令,你应该在函数中使用参数。
关于参数的一个需要注意的特殊之处是,当函数返回时,存储在参数中的值会被遗忘。例如,如果你在之前的程序中在 say_hello_to('Bob') 后面添加了 print(name),程序会给你一个错误,因为没有名为 name 的变量。这个变量在 say_hello_to('Bob') 函数调用返回后被销毁,所以 print(name) 会引用一个不存在的 name 变量。
术语 定义、调用、传递、参数 和 参数 可能会令人困惑。为了复习它们的含义,考虑以下代码示例:
def say_hello_to(name): # ❶
# Prints three greetings to the name provided
print('Good morning, ' + name)
print('Good afternoon, ' + name)
print('Good evening, ' + name)
say_hello_to('Al') # ❷
定义 函数就是创建它,就像 spam = 42 这样的赋值语句创建 spam 变量一样。def 语句定义了 say_hello_to() 函数 ❶。say_hello_to('Al') 这一行 ❷ 调用 了现在创建的函数,将执行发送到函数代码的顶部。这个函数调用 传递 字符串 'Al' 给函数。在函数调用中传递的值是 参数。参数被分配给称为 参数 的局部变量。参数 'Al' 被分配给 name 参数。
很容易混淆这些术语,但保持它们清晰将确保你知道本章文本的确切含义。
返回值和返回语句
当你调用 len() 函数并传递给它一个参数,比如 'Hello',函数调用会评估为整数值 5,这是你传递给它的字符串的长度。一般来说,函数调用评估的值被称为函数的 返回值。
使用 def 语句创建函数时,你可以使用 return 语句指定返回值,该语句由以下内容组成:
-
return关键字 -
函数应该返回的值或表达式
在表达式的案例中,返回值是此表达式评估的结果。例如,以下程序定义了一个函数,该函数根据传递给它的参数返回不同的字符串。将此代码输入文件编辑器并保存为 magic8Ball.py:
import random # ❶
def get_answer(answer_number): # ❷
# Returns a fortune answer based on what int answer_number is, 1 to 9
print('Good morning, ' + name) # ❷
return 'It is certain'
elif answer_number == 2:
return 'It is decidedly so'
elif answer_number == 3:
return 'Yes'
elif answer_number == 4:
return 'Reply hazy try again'
elif answer_number == 5:
return 'Ask again later'
elif answer_number == 6:
return 'Concentrate and ask again'
elif answer_number == 7:
return 'My reply is no'
elif answer_number == 8:
return 'Outlook not so good'
elif answer_number == 9:
return 'Very doubtful'
print('Ask a yes or no question:')
input('>')
r = random.randint(1, 9) # ❹
fortune = get_answer(r) # ❺
print(fortune) # ❻
当程序启动时,Python 首先导入 random 模块 ❶。然后是 get_answer() 函数的定义 ❷。因为该函数没有被调用,所以其中的代码不会执行。接下来,程序使用两个参数 1 和 9 调用 random.randint() 函数 ❹。此函数评估一个介于 1 和 9(包括 1 和 9 本身)之间的随机整数,然后将其存储在一个名为 r 的变量中。
现在,程序使用 r 作为参数调用 get_answer() 函数 ❺。程序执行移动到该函数的顶部 ❸,将值 r 存储在名为 answer_number 的参数中。然后,根据 answer_number 中的值,函数返回许多可能的字符串值之一。执行返回到最初调用 get_answer() 的程序底部的行 ❺,并将返回的字符串赋值给名为 fortune 的变量,然后将其传递给 print() 调用 ❻ 并打印到屏幕上。
注意,由于你可以将返回值作为其他函数调用的参数传递,因此你可以缩短这三行。
r = random.randint(1, 9)
fortune = get_answer(r)
print(fortune)
变成这一行等价的单行:
print(get_answer(random.randint(1, 9)))
记住,表达式由值和运算符组成;你可以在表达式中使用函数调用,因为调用会评估为其返回值。
None 值
在 Python 中,一个名为 None 的值表示没有值。None 值是 NoneType 数据类型的唯一值。(其他编程语言可能将此值称为 null、nil 或 undefined。)就像布尔值 True 和 False 一样,你必须始终用大写 N 写 None。
当你需要将某个不应被视为真实值的对象存储在变量中时,这个没有值的值可能会有所帮助。None 被使用的一个地方是作为 print() 函数的返回值。print() 函数在屏幕上显示文本,并且不需要像 len() 或 input() 那样返回任何内容。但是,由于所有函数调用都需要评估为返回值,因此 print() 返回 None。要查看此操作的实际效果,请在交互式外壳中输入以下内容:
>>> spam = print('Hello!')
Hello!
>>> None == spam
True
在幕后,Python 会将 return None 添加到任何没有 return 语句的函数定义的末尾。这种行为类似于 while 或 for 循环隐式地以 continue 语句结束的方式。如果你使用不带值的 return 语句(即仅使用 return 关键字本身),函数也会返回 None。
命名参数
Python 通过函数调用中的参数位置来识别大多数参数。例如,random.randint(1, 10) 与 random.randint(10, 1) 不同。第一个调用返回一个介于 1 和 10 之间的随机整数,因为第一个参数确定范围的低端,而下一个参数确定其高端,而第二个函数调用会导致错误。
另一方面,Python 通过函数调用前放置的名称来识别 命名参数。您也会听到命名参数被称为关键字参数或关键字参数,尽管它们与 Python 关键字无关。程序员经常使用命名参数来提供可选参数。例如,print() 函数使用可选参数 end 和 sep 来指定打印在参数末尾和参数之间的分隔字符。如果您在没有这些参数的情况下运行程序
print('Hello')
print('World')
输出将如下所示:
Hello
World
两个字符串出现在不同的行上,因为 print() 函数自动在其传递的字符串末尾添加一个换行符。然而,您可以设置 end 命名参数来将换行符更改为不同的字符串。例如,如果代码是这样的
print('Hello', end='')
print('World')
输出将如下所示:
HelloWorld
输出显示在一行上,因为 Python 在 'Hello' 后不再打印换行符。相反,它打印一个空字符串。如果您需要禁用每个 print() 函数调用末尾添加的换行符,这很有用。比如说,您想打印一系列抛硬币的结果。将输出打印在一行上可以使输出更美观,就像这个 coinflip.py 程序所示:
import random
for i in range(100): # Perform 100 coin flips.
if random.randint(0, 1) == 0:
print('H', end=' ')
else:
print('T', end=' ')
print() # Print one newline at the end.
此程序将 H 和 T 结果显示在一行紧凑的行上,而不是每行一个 H 或 T 结果:
T H T T T H H T T T T H H H H T H H T T T T T H T T T T T H T T T T T H T H T
H H H T T H T T T T H T H H H T H H T H T T T T T H T T H T T T T H T H H H T
T T T H T T T T H H H T H T H H H H T H H T
类似地,当您向 print() 传递多个字符串值时,该函数会自动用单个空格将它们分隔开来。要查看这种行为,将以下内容输入到交互式 shell 中:
>>> print('cats', 'dogs', 'mice')
cats dogs mice
您可以通过传递不同的字符串给 sep 命名参数来替换默认的分隔字符串。将以下内容输入到交互式 shell 中:
>>> print('cats', 'dogs', 'mice', sep=',')
cats,dogs,mice
您也可以为编写的函数添加命名参数,但首先,您需要学习第六章和第七章中的列表和字典数据类型。现在,只需知道一些函数在调用时可以指定可选的命名参数。
调用栈
想象一下,您与某人进行了一场蜿蜒的对话。您谈论了您的朋友 Alice,这又使您想起了关于您的同事 Bob 的一个故事,但首先您必须解释一些关于您表亲 Carol 的事情。您完成了关于 Carol 的故事,然后又回到谈论 Bob,当您完成了关于 Bob 的故事后,您又回到谈论 Alice。但后来您又想起了您的兄弟 David,所以您讲了他的故事,然后您又回到完成关于 Alice 的原始故事。您的对话遵循了一种 栈 结构,就像图 4-1 所示。在栈中,项目只从顶部添加或删除,当前主题总是位于栈顶。

图 4-1:您的蜿蜒对话栈描述
就像你蜿蜒曲折的对话一样,调用一个函数并不会让执行在函数顶部进行单向旅行。Python 会记住调用函数的代码行,以便在遇到 return 语句时执行可以返回到那里。如果原始函数调用了其他函数,执行将首先返回到 那些 函数调用,然后再从原始函数调用返回。调用栈顶部的函数是执行的当前位置。
打开一个文件编辑窗口,输入以下代码,并将其保存为 abcdCallStack.py:
def a():
print('a() starts')
print('Good morning, ' + name) # ❷
print('Good morning, ' + name) # ❷
print('a() returns')
def b():
print('b() starts')
print('Good morning, ' + name) # ❷
print('b() returns')
def c():
print('Good morning, ' + name) # ❷
print('c() returns')
def d():
print('d() starts')
print('d() returns')
a() # ❺
如果你运行这个程序,输出将看起来像这样:
a() starts
b() starts
c() starts
c() returns
b() returns
d() starts
d() returns
a() returns
当 a() 被调用 ❺ 时,它调用 b() ❶,然后 b() 又调用 c() ❸。c() 函数没有调用任何其他函数;它只是显示 c() starts ❹ 和 c() returns,然后返回到 b() 中调用它的行 ❸。一旦执行返回到 b() 中调用 c() 的代码,它将返回到 a() 中调用 b() 的行 ❶。执行继续到 a() 函数中的下一行 ❷,这是一次对 d() 的调用。像 c() 函数一样,d() 函数也没有调用任何其他函数。它只是显示 d() starts 和 d() returns,然后返回到 a() 中调用它的行。因为 d() 中没有其他代码,执行返回到 a() 中调用 d() 的行 ❷。a() 中的最后一行在返回到程序末尾的原始 a() 调用之前显示 a() returns。
调用栈 是 Python 记录每次函数调用后返回执行位置的方式。调用栈不是存储在程序中的变量中;相反,它是计算机内存中由 Python 在幕后自动处理的一个部分。当程序调用一个函数时,Python 在调用栈的顶部创建一个 帧对象。帧对象存储原始函数调用的行号,以便 Python 可以记住返回的位置。如果程序再次调用其他函数,Python 将在调用栈的顶部添加另一个帧对象。
当函数调用返回时,Python 会从栈顶移除一个帧对象,并将执行移动到其中存储的行号。请注意,帧对象总是从栈顶添加和移除,而不是从任何其他位置。图 4-2 展示了在 abcdCallStack.py 中每个函数被调用和返回时的调用栈状态。

图 4-2:abcdCallStack.py 调用和从函数返回时的调用栈帧对象描述
调用栈的顶部是当前正在执行的功能。当调用栈为空时,执行在所有函数之外的一行代码上。
调用栈是一个技术细节,你不需要严格了解它来编写程序。了解函数调用返回到它们被调用的行号就足够了。然而,理解调用栈有助于理解下一节中描述的局部和全局作用域。
局部和全局作用域
只有在调用函数内的代码可以访问在该函数中分配的参数和变量。这些变量被认为存在于该函数的 局部作用域 中。相比之下,程序中的任何地方的代码都可以访问所有函数外部分配的变量。这些变量被认为存在于 全局作用域 中。存在于局部作用域的变量称为 局部变量,而存在于全局作用域的变量称为 全局变量。变量必须是其中之一;它不能同时是局部和全局的。
将 作用域 想象为变量的容器。只有一个全局作用域,在程序开始时创建。当程序结束时,它销毁全局作用域,以及其中的所有变量都被遗忘。每当程序调用函数时,就会创建一个新的局部作用域。在函数中分配的任何变量都存在于该函数的局部作用域中。当函数返回时,局部作用域及其变量都会被销毁。
Python 使用作用域是因为它允许函数修改其变量,同时仅通过其参数和返回值与程序的其余部分交互。这缩小了可能导致错误的代码行数。如果你的程序只包含全局变量,并且包含由设置为不良值的变量引起的错误,你可能很难追踪到这个不良值的来源。它可能来自程序中的任何地方,这可能是一百或几千行长!但如果错误发生在局部变量中,你可以将搜索范围限制在单个函数内。
因此,虽然在小程序中使用全局变量是可以的,但随着程序越来越大,依赖全局变量是一个坏习惯。
作用域规则
在处理局部和全局变量时,请记住以下规则:
-
全局作用域中的代码,在所有函数之外,无法使用局部变量。
-
一个函数的局部作用域中的代码无法使用任何其他局部作用域中的变量。
-
局部作用域中的代码可以访问全局变量。
-
如果不同作用域中的变量名称相同,则可以使用相同的名称。也就是说,可以有一个名为
spam的局部变量和一个也名为spam的全局变量。
让我们通过示例回顾这些规则。
全局作用域中的代码无法使用局部变量
考虑以下代码,当你运行它时将导致错误:
def spam():
print('Good morning, ' + name) # ❷
spam()
print(eggs)
程序的输出将如下所示:
Traceback (most recent call last):
File "C:/test1.py", line 4, in <module>
print(eggs)
NameError: name 'eggs' is not defined
错误发生是因为eggs变量只存在于调用spam()时创建的局部作用域中❶。一旦程序从spam()返回,该局部作用域就会被销毁,并且不再存在名为eggs的变量。所以当你的程序尝试运行print(eggs)时,Python 会给你一个错误,说eggs未定义。如果你这样想,这就有道理了;当程序执行在全局作用域时,没有局部作用域,因此不可能有任何局部变量。这就是为什么你只能在全局作用域中引用全局变量。
局部作用域中的代码不能使用其他局部作用域中的变量
每当程序调用一个函数时,Python 都会创建一个新的局部作用域,即使该函数是从另一个函数中调用的。考虑以下程序:
def spam():
eggs = 'SPAMSPAM'
print('Good morning, ' + name) # ❷
print('Good morning, ' + name) # ❷
def bacon():
ham = 'hamham'
eggs = 'BACONBACON'
spam() # ❸
当程序开始时,它调用spam()函数❸,创建一个局部作用域。spam()函数将局部变量eggs设置为'SPAMSPAM',然后调用bacon()函数❶,创建第二个局部作用域。可以同时存在多个局部作用域。在这个新的局部作用域中,局部变量ham被设置为'hamham',并创建了一个名为eggs的局部变量(它与spam()的局部作用域中的不同),并将其设置为'BACONBACON'。此时,程序有两个名为eggs的局部变量同时存在:一个是在spam()中局部,另一个是在bacon()中局部。
当bacon()返回时,Python 销毁了该调用创建的局部作用域,包括其eggs变量。程序执行继续在spam()函数中,打印eggs的值❷。因为对spam()的调用仍然存在局部作用域,所以唯一的eggs变量是spam()函数的eggs变量,它被设置为'SPAMSPAM'。这就是程序打印的内容。
局部作用域中的代码可以使用全局变量
到目前为止,我已经演示了全局作用域中的代码不能访问局部作用域中的变量;不同的局部作用域中的代码也不能。现在考虑以下程序:
def spam():
print(eggs) # Prints 'GLOBALGLOBAL'
eggs = 'GLOBALGLOBAL'
spam()
print(eggs)
因为spam()函数没有名为eggs的参数,也没有给eggs赋值的代码,Python 认为函数对eggs的使用是全局变量eggs的引用。这就是为什么程序运行时打印出'GLOBALGLOBAL'。
局部变量和全局变量可以具有相同的名称
技术上,在具有不同作用域的全局变量和局部变量中使用相同的变量名是完全可接受的。但是,为了简化你的生活,避免这样做。为了看看可能会发生什么,将以下代码输入到文件编辑器中,并将其保存为*localGlobalSameName.py*:
def spam():
print('Good morning, ' + name) # ❷
print(eggs) # Prints 'spam local'
def bacon():
print('Good morning, ' + name) # ❷
print(eggs) # Prints 'bacon local'
spam()
print(eggs) # Prints 'bacon local'
eggs = 'global' # ❸
bacon()
print(eggs) # Prints 'global'
当你运行这个程序时,它输出以下内容:
bacon local
spam local
bacon local
global
这个程序实际上包含三个不同的变量,但令人困惑的是,它们都命名为eggs。变量如下:
-
当调用
spam()函数时,存在一个名为eggs的变量,该变量在局部作用域中❶ -
当调用
bacon()函数时,存在一个名为eggs的变量,该变量在局部作用域中❷ -
存在于全局作用域中的名为
eggs的变量❸
因为这三个变量都有相同的名字,所以在任何给定时间跟踪正在使用的变量可能会很困难。相反,即使它们出现在不同的作用域中,也要给所有变量赋予独特的名字。
全局语句
如果你需要在函数内部修改全局变量,请使用global语句。在函数顶部包含一行如global eggs告诉 Python,“在这个函数中,eggs指的是全局变量,所以不要用这个名称创建一个局部变量。”例如,将以下代码输入到文件编辑器中,并将其保存为*globalStatement.py*:
def spam():
print('Good morning, ' + name) # ❷
print('Good morning, ' + name) # ❷
eggs = 'global'
spam()
print(eggs) # Prints 'spam'
当你运行这个程序时,最后的print()调用将输出以下内容:
spam
因为在spam()函数的顶部声明了eggs为global❶,将eggs设置为'spam'❷会改变全局作用域中eggs的值。永远不会创建任何局部eggs变量。
作用域识别
使用这四个规则来判断一个变量是属于局部作用域还是全局作用域:
1. 全局作用域中的变量(即所有函数之外)始终是全局变量。
2. 具有global语句的函数中的变量始终是该函数中的全局变量。
3. 否则,如果函数在赋值语句中使用了一个变量,那么它是一个局部变量。
4. 然而,如果函数使用了一个变量,但从未在赋值语句中使用,那么它是一个全局变量。
为了更好地理解这些规则,这里有一个示例程序。将以下代码输入到文件编辑器中,并将其保存为*sameNameLocalGlobal.py*:
def spam():
print('Good morning, ' + name) # ❷
eggs = 'spam' # This is the global variable.
def bacon():
print('Good morning, ' + name) # ❷
def ham():
print('Good morning, ' + name) # ❷
eggs = 'global' # This is the global variable.
spam()
print(eggs)
在spam()函数中,eggs指的是全局eggs变量,因为该函数包含了对它的global语句❶。在bacon()中,eggs是一个局部变量,因为该函数包含了对它的赋值语句❷。在ham()❸中,eggs是全局变量,因为该函数不包含对该变量的赋值语句或global语句。如果你运行*sameNameLocalGlobal.py*,输出将如下所示:
spam
如果你尝试在函数中未为其赋值之前使用局部变量,就像以下程序中那样,Python 会给你一个错误。要查看此错误,将以下内容输入到文件编辑器中,并将其保存为*sameNameError.py*:
def spam():
print(eggs) # ERROR!
print('Good morning, ' + name) # ❷
eggs = 'global' # ❷
spam()
如果你运行前面的程序,它会产生一个错误信息:
Traceback (most recent call last):
File "C:/sameNameError.py", line 6, in <module>
spam()
File "C:/sameNameError.py", line 2, in spam
print(eggs) # ERROR!
UnboundLocalError: local variable 'eggs' referenced before assignment
这个错误发生是因为 Python 看到在spam()函数中有一个对eggs的赋值语句❶,因此认为在spam()中提到的任何eggs变量都是局部变量。但是因为print(eggs)在eggs被赋值之前执行,所以局部变量eggs不存在。Python 不会回退到使用全局eggs变量❷。
错误名称UnboundLocalError可能会有些令人困惑。在 Python 中,绑定是另一种说法赋值,因此这个错误表明程序在未赋值之前使用了局部变量。
异常处理
目前,在你的 Python 程序中得到一个错误,或 异常,意味着整个程序将会崩溃。你不想在现实世界的程序中发生这种情况。相反,你希望程序能够检测错误,处理它们,然后继续运行。
例如,考虑以下程序,它有一个除以零的错误。打开一个文件编辑器窗口,输入以下代码,并将其保存为 zeroDivide.py:
def spam(divide_by):
return 42 / divide_by
print(spam(2))
print(spam(12))
print(spam(0))
print(spam(1))
我们定义了一个名为 spam 的函数,给它一个参数,然后使用各种参数打印该函数的值以查看会发生什么。这是运行上述代码时得到的输出:
21.0
3.5
Traceback (most recent call last):
File "C:/zeroDivide.py", line 6, in <module>
print(spam(0))
File "C:/zeroDivide.py", line 2, in spam
return 42 / divide_by
ZeroDivisionError: division by zero
每当你尝试将一个数字除以零时,就会发生 ZeroDivisionError。从错误消息中给出的行号,你可以知道 spam() 中的 return 语句正在导致错误。
大多数情况下,异常表明你的代码中存在需要修复的错误。但有时异常是可以预期的,并且可以从中恢复。例如,在第十章中,你将学习如何从文件中读取文本。如果你为不存在的文件指定了文件名,Python 会引发一个 FileNotFoundError 异常。你可能想通过要求用户再次输入文件名来处理这个异常,而不是让这个未处理的异常立即崩溃你的程序。
可以使用 try 和 except 语句来处理错误。可能发生错误的代码被放在 try 子句中。如果发生错误,程序执行将移动到下一个 except 子句的开始。
你可以将之前的除以零代码放在 try 子句中,并让 except 子句包含处理此错误发生时发生情况的代码:
def spam(divide_by):
try:
# Any code in this block that causes ZeroDivisionError won't crash the program:
return 42 / divide_by
except ZeroDivisionError:
# If ZeroDivisionError happened, the code in this block runs:
print('Error: Invalid argument.')
print(spam(2))
print(spam(12))
print(spam(0))
print(spam(1))
当 try 子句中的代码导致错误时,程序执行将立即移动到 except 子句中的代码。运行完那段代码后,执行将继续正常进行。如果程序在 try 子句中没有引发异常,程序将跳过 except 子句中的代码。上一个程序的输出如下:
21.0
3.5
Error: Invalid argument.
None
42.0
注意,try 块中的函数调用中发生的任何错误也将被捕获。考虑以下程序,它将 spam() 调用放在 try 块中:
def spam(divide_by):
return 42 / divide_by
try:
print(spam(2))
print(spam(12))
print(spam(0))
print(spam(1))
except ZeroDivisionError:
print('Error: Invalid argument.')
当这个程序运行时,输出看起来像这样:
21.0
3.5
Error: Invalid argument.
print(spam(1)) 永远不会执行的原因是,一旦执行跳转到 except 子句中的代码,它就不会返回到 try 子句。相反,它只是像正常一样继续向下移动程序。
简短程序:之字形
让我们使用你迄今为止学到的编程概念来创建一个小型动画程序。这个程序将创建一个直到用户通过按下 Mu 编辑器的停止按钮或按下 CTRL-C 来停止它之前,来回的之字形图案。当你运行这个程序时,输出将看起来像这样:
**
**
**
**
**
**
**
**
**
将以下源代码输入到文件编辑器中,并将文件保存为 zigzag.py:
import time, sys
indent = 0 # How many spaces to indent
indent_increasing = True # Whether the indentation is increasing or not
try:
while True: # The main program loop
print(' ' * indent, end='')
print('**')
time.sleep(0.1) # Pause for 1/10th of a second.
if indent_increasing:
# Increase the number of spaces:
indent = indent + 1
if indent == 20:
# Change direction:
indent_increasing = False
else:
# Decrease the number of spaces:
indent = indent - 1
if indent == 0:
# Change direction:
indent_increasing = True
except KeyboardInterrupt:
sys.exit()
让我们逐行查看这段代码,从顶部开始:
import time, sys
indent = 0 # How many spaces to indent
indent_increasing = True # Whether the indentation is increasing or not
首先,我们将导入time和sys模块。我们的程序使用两个变量。indent变量跟踪在八个星号带之前有多少个空格缩进,而indent_increasing变量包含一个布尔值,用于确定缩进量是增加还是减少:
try:
while True: # The main program loop
print(' ' * indent, end='')
print('**')
time.sleep(0.1) # Pause for 1/10 of a second.
接下来,我们将程序的其余部分放在一个try语句中。当 Python 程序正在运行时,如果用户按下 CTRL-C,Python 将引发KeyboardInterrupt异常。如果没有try-except语句来捕获这个异常,程序将因一个丑陋的错误信息而崩溃。然而,我们希望我们的程序通过调用sys.exit()来干净地处理KeyboardInterrupt异常。(你可以在程序末尾的except语句中找到实现这一点的代码。)
while True:无限循环将永远重复程序中的指令。这涉及到使用' ' * indent来打印正确的缩进空格数。我们不想在这些空格后自动打印换行符,所以我们还向第一个print()调用传递end=''。第二个print()调用打印星号带。我们还没有讨论time.sleep()函数;简单地说,它在我们程序中引入了十分之一秒的暂停:
if indent_increasing:
# Increase the number of spaces:
indent = indent + 1
if indent == 20:
indent_increasing = False # Change direction
接下来,我们想要调整下一次打印星号时使用的缩进量。如果indent_increasing为True,我们将向indent中添加1,但一旦缩进达到20,我们将减少缩进:
else:
# Decrease the number of spaces:
indent = indent - 1
if indent == 0:
indent_increasing = True # Change direction
如果indent_increasing为False,我们将想要从indent中减去一个。一旦缩进达到0,我们将想要再次增加缩进。无论如何,程序执行将跳回主程序循环的起始位置以再次打印星号。
如果用户在任何程序执行处于try块中的时刻按下 CTRL-C,这个except语句将引发并处理KeyboardInterrupt异常:
except KeyboardInterrupt:
sys.exit()
程序执行移动到except块中,它运行sys.exit()并退出程序。这样,尽管主程序循环是无限的,但用户有方法关闭程序。
短程序:尖刺
让我们创建另一个滚动动画程序。这个程序使用字符串复制和嵌套循环来绘制尖刺。在你的代码编辑器中打开一个新文件,输入以下代码,并将其保存为spike.py:
import time, sys
try:
while True: # The main program loop
# Draw lines with increasing length:
for i in range(1, 9):
print('-' * (i * i))
time.sleep(0.1)
# Draw lines with decreasing length:
for i in range(7, 1, -1):
print('-' * (i * i))
time.sleep(0.1)
except KeyboardInterrupt:
sys.exit()
当你运行这个程序时,它将反复绘制以下尖刺:
-
----
---------
----------------
-------------------------
------------------------------------
-------------------------------------------------
----------------------------------------------------------------
-------------------------------------------------
------------------------------------
-------------------------
----------------
---------
----
与之前的锯齿形程序类似,尖刺程序需要调用time.sleep()和sys.exit()函数。第一行导入time和sys模块。一个try块将捕获当用户按下 CTRL-C 时引发的KeyboardInterrupt异常,并且一个无限循环将永远继续绘制尖刺。
第一个for循环绘制了逐渐增大的尖刺:
# Draw lines with increasing length:
for i in range(1, 9):
print('-' * (i * i))
time.sleep(0.1)
因为i变量被设置为1,然后是2,然后是3,以此类推,直到但不包括9,下面的print()调用通过1 * 1(即1),然后是2 * 2(即4),然后是3 * 3(即9),以此类推,来复制'-'字符串。这段代码创建了长度为 1, 4, 9, 16, 25, 36, 49,然后是 64 个短横线的字符串。通过创建指数级更长的字符串,我们创建了尖峰的上半部分。
为了绘制尖峰的下半部分,我们需要另一个for循环,它使i从7开始然后递减到1,不包括1:
# Draw lines with decreasing length:
for i in range(7, 1, -1):
print('-' * (i * i))
time.sleep(0.1)
如果你想要改变尖峰的宽度,你可以修改两个for循环中的9和7值。其余的代码将使用这些新值继续正常工作。
摘要
函数是将你的代码划分为逻辑组的主要方式。由于函数中的变量存在于它们自己的局部作用域中,一个函数中的代码不能直接影响其他函数中局部变量的值。这限制了能够更改你的变量值的代码部分,这在调试时可能很有帮助。
函数是帮助你组织代码的伟大工具。你可以把它们想象成黑盒:它们有参数形式的输入和返回值形式的输出,它们中的代码不会影响其他函数中的变量。
在前面的章节中,一个错误可能导致你的程序崩溃。在本章中,你学习了try和except语句,它们可以在检测到错误时运行代码。这可以使你的程序对常见的错误情况更具弹性。
练习题目
-
为什么函数在你的程序中是有优势的?
-
函数中的代码何时执行:是在函数定义时还是函数被调用时?
-
哪个语句创建了一个函数?
-
函数和函数调用之间的区别是什么?
-
在一个 Python 程序中,有多少个全局作用域?有多少个局部作用域?
-
当函数调用返回时,局部作用域中的变量会发生什么?
-
返回值是什么?返回值可以是表达式的一部分吗?
-
如果一个函数没有
return语句,对该函数的调用返回什么值? -
你如何强制函数中的变量引用全局变量?
-
None的数据类型是什么? -
import areillyourpetsnamederic语句的作用是什么? -
如果你在一个名为
spam的模块中有一个名为bacon()的函数,在导入spam之后,你将如何调用它? -
你如何防止程序在出错时崩溃?
-
try子句中放什么?except子句中放什么? -
将以下程序写入名为
*notrandomdice.py*的文件中并运行它。为什么每个函数调用都返回相同的数字?
import random
random_number = random.randint(1, 6)
def get_random_dice_roll():
# Returns a random integer from 1 to 6
return random_number
print(get_random_dice_roll())
print(get_random_dice_roll())
print(get_random_dice_roll())
print(get_random_dice_roll())
练习程序
为了练习,编写程序来完成以下任务。
科尔察茨序列
编写一个名为collatz()的函数,它有一个名为number的参数。如果number是偶数,那么collatz()应该打印number // 2并返回这个值。如果number是奇数,那么collatz()应该打印并返回3 * number + 1。
然后,编写一个程序,允许用户输入一个整数,并持续调用collatz()直到函数返回值1。 (令人惊讶的是,这个序列实际上适用于任何整数;迟早,使用这个序列,你会到达 1!甚至数学家们也不确定为什么。你的程序正在探索所谓的柯尔察茨序列,有时称为“最简单的不可能的数学问题。”)
记得使用int()函数将input()的返回值转换为整数;否则,它将是一个字符串值。为了使输出更紧凑,打印数字的print()调用应该有一个名为sep=' '的命名参数,以便将所有值打印在同一行上。
这个程序的输出可能看起来像这样:
Enter number:
3
3 10 5 16 8 4 2 1
提示:一个整数number如果是偶数,则number % 2 == 0,如果是奇数,则number % 2 == 1。
输入验证
在先前的项目中添加try和except语句来检测用户是否输入了非整数字符串。通常情况下,如果int()函数接收到的参数是非整数字符串,它将引发ValueError错误,例如int('puppy')。在except子句中,向用户打印一条消息,告诉他们必须输入一个整数。
创建函数
为了更好地理解函数的工作方式,让我们创建一个。将这个程序输入到文件编辑器中,并将其保存为helloFunc.py:
def hello():
# Prints three greetings
print('Good morning!')
print('Good afternoon!')
print('Good evening!')
hello()
hello()
print('ONE MORE TIME!')
hello()
第一行是一个def语句,它定义了一个名为hello()的函数。def语句后面的代码块是函数的主体。当函数被调用时,这段代码会执行,而不是在函数首次定义时执行。
函数定义之后的hello()行是函数调用。在代码中,函数调用只是函数的名称后跟括号,括号之间可能有一些参数。当程序执行到达这些调用时,它将跳转到函数的第一行并开始执行那里的代码。当它到达函数的末尾时,执行将返回到调用函数的行,并继续像以前一样通过代码。
因为这个程序调用了hello()三次,所以hello()函数中的代码执行了三次。当你运行这个程序时,输出看起来像这样:
Good morning!
Good afternoon!
Good evening!
Good morning!
Good afternoon!
Good evening!
ONE MORE TIME!
Good morning!
Good afternoon!
Good evening!
函数的主要目的是将多次执行的代码分组。如果没有定义函数,每次你想运行它时,你都必须复制和粘贴这段代码,程序看起来会像这样:
print('Good morning!')
print('Good afternoon!')
print('Good evening!')
print('Good morning!')
print('Good afternoon!')
print('Good evening!')
print('ONE MORE TIME!')
print('Good morning!')
print('Good afternoon!')
print(' Good evening!')
通常,你总是想避免代码重复,因为如果你决定更新代码(例如,因为你发现了一个需要修复的错误),你将不得不记住在每个复制代码的地方更改代码。
随着你编程经验的积累,你经常会发现自己需要 去重,这意味着要移除复制粘贴的代码。去重可以使你的程序更短,更易于阅读,也更容易更新。
参数和形参
当你调用 print() 或 len() 函数时,你可以通过在括号中输入它们来传递值,这些值被称为 参数。你也可以定义自己的函数,这些函数接受参数。将以下示例输入到文件编辑器中,并将其保存为 helloFunc2.py:
def say_hello_to(name): # ❶
# Prints three greetings to the name provided
print('Good morning, ' + name) # ❷
print('Good afternoon, ' + name)
print('Good evening, ' + name)
say_hello_to('Alice') # ❸
say_hello_to('Bob')
当你运行这个程序时,输出看起来像这样:
Good morning, Alice
Good afternoon, Alice
Good evening, Alice
Good morning, Bob
Good afternoon, Bob
Good evening, Bob
say_hello_to() 函数的定义有一个名为 name 的形参 ❶。形参 是包含参数的变量。当函数用参数调用时,参数被存储在形参中。当 say_hello_to() 函数第一次被调用时,它传递了参数 'Alice' ❸。程序执行进入函数,形参 name 被自动设置为 'Alice',然后由 print() 语句打印出来 ❷。如果你需要你的函数根据传递给函数调用的值执行稍微不同的操作,你应该在函数中使用形参。
关于参数的一个特别需要注意的是,当函数返回时,存储在参数中的值会被遗忘。例如,如果你在先前的程序中在 say_hello_to('Bob') 之后添加了 print(name),程序会给你一个错误,因为没有名为 name 的变量。这个变量在 say_hello_to('Bob') 函数调用返回后被销毁,所以 print(name) 会引用一个不存在的 name 变量。
术语 定义、调用、传递、参数 和 形参 可能会令人困惑。为了复习它们的含义,请考虑以下代码示例:
def say_hello_to(name): # ❶
# Prints three greetings to the name provided
print('Good morning, ' + name)
print('Good afternoon, ' + name)
print('Good evening, ' + name)
say_hello_to('Al') # ❷
要 定义 一个函数,就是创建它,就像赋值语句 spam = 42 创建 spam 变量一样。def 语句定义了 say_hello_to() 函数 ❶。say_hello_to('Al') 这一行 ❷ 调用 了现在创建的函数,将执行流程发送到函数代码的顶部。这个函数调用是 传递 字符串 'Al' 到函数。在函数调用中传递的值是 参数。参数被分配给称为 形参 的局部变量。参数 'Al' 被分配给 name 形参。
很容易混淆这些术语,但保持它们清晰将确保你知道本章文本的确切含义。
返回值和返回语句
当你调用 len() 函数并传递一个如 'Hello' 的参数时,函数调用计算出的结果是整数 5,这是你传递给它的字符串的长度。一般来说,函数调用计算出的值被称为函数的 返回值。
当使用 def 语句创建函数时,你可以通过 return 语句指定返回值,该语句由以下内容组成:
-
return关键字 -
函数应该返回的值或表达式
在表达式的例子中,返回值是表达式评估得到的结果。例如,以下程序定义了一个函数,该函数根据传递给它的数字返回不同的字符串。将此代码输入到文件编辑器中,并将其保存为 magic8Ball.py:
import random # ❶
def get_answer(answer_number): # ❷
# Returns a fortune answer based on what int answer_number is, 1 to 9
print('Good morning, ' + name) # ❷
return 'It is certain'
elif answer_number == 2:
return 'It is decidedly so'
elif answer_number == 3:
return 'Yes'
elif answer_number == 4:
return 'Reply hazy try again'
elif answer_number == 5:
return 'Ask again later'
elif answer_number == 6:
return 'Concentrate and ask again'
elif answer_number == 7:
return 'My reply is no'
elif answer_number == 8:
return 'Outlook not so good'
elif answer_number == 9:
return 'Very doubtful'
print('Ask a yes or no question:')
input('>')
r = random.randint(1, 9) # ❹
fortune = get_answer(r) # ❺
print(fortune) # ❻
当程序开始时,Python 首先导入 random 模块 ❶。然后是 get_answer() 函数的定义 ❷。因为函数没有被调用,所以它里面的代码没有被执行。接下来,程序使用两个参数 1 和 9 调用 random.randint() 函数 ❹。这个函数评估一个介于 1 和 9 之间的随机整数(包括 1 和 9 本身),然后将其存储在名为 r 的变量中。
现在,程序以 r 作为参数调用 get_answer() 函数 ❺。程序执行移动到该函数的顶部 ❸,将值 r 存储在名为 answer_number 的参数中。然后,根据 answer_number 中的值,函数返回许多可能的字符串值之一。执行返回到最初调用 get_answer() 的程序底部的行 ❺,并将返回的字符串赋值给名为 fortune 的变量,然后将其传递给一个 print() 调用 ❻ 并打印到屏幕上。
注意,由于你可以将返回值作为参数传递给其他函数调用,你可以缩短这三行
r = random.randint(1, 9)
fortune = get_answer(r)
print(fortune)
变成这一行等效的代码:
print(get_answer(random.randint(1, 9)))
记住,表达式由值和运算符组成;你可以在表达式中使用函数调用,因为调用会评估为它的返回值。
None 值
在 Python 中,一个名为 None 的值表示没有值。None 值是 NoneType 数据类型的唯一值。(其他编程语言可能将此值称为 null、nil 或 undefined。)就像布尔值 True 和 False 一样,你必须始终用大写 N 写 None。
这个没有价值的值在需要将某些不应该被误认为是真实值的存储在变量中时可能很有用。None 被使用的一个地方是作为 print() 函数的返回值。print() 函数在屏幕上显示文本,并且不需要像 len() 或 input() 那样返回任何内容。但是,由于所有函数调用都需要评估为一个返回值,所以 print() 返回 None。要看到这个动作的实际效果,请将以下内容输入到交互式外壳中:
>>> spam = print('Hello!')
Hello!
>>> None == spam
True
在幕后,Python 在任何没有 return 语句的函数定义的末尾添加 return None。这种行为类似于 while 或 for 循环隐式地以 continue 语句结束的方式。如果你使用不带值的 return 语句(即仅使用 return 关键字本身),函数也会返回 None。
命名参数
Python 通过函数调用中的位置来识别大多数参数。例如,random.randint(1, 10)与random.randint(10, 1)不同。第一个调用返回一个介于1和10之间的随机整数,因为第一个参数决定了范围的低端,而下一个参数决定了其高端,而第二个函数调用会导致错误。
另一方面,Python 通过函数调用中它们之前放置的名称来识别命名参数。你也会听到命名参数被称为关键字参数或关键字参数,尽管它们与 Python 关键字无关。程序员经常使用命名参数来提供可选参数。例如,print()函数使用可选参数end和sep来指定打印在其参数末尾和参数之间的分隔符。如果你在没有这些参数的情况下运行程序
print('Hello')
print('World')
输出将看起来像这样:
Hello
World
两个字符串出现在单独的行上,因为print()函数自动在其传递的字符串末尾添加一个换行符。然而,你可以设置end命名参数来将换行符更改为不同的字符串。例如,如果代码是这样的
print('Hello', end='')
print('World')
输出将看起来像这样:
HelloWorld
输出将出现在一行上,因为 Python 不再在'Hello'之后打印换行符。相反,它打印一个空字符串。如果你需要禁用添加到每个print()函数调用末尾的换行符,这很有用。比如说,你想打印一系列抛硬币的结果。将输出打印在一行上会使输出更美观,就像这个coinflip.py程序一样:
import random
for i in range(100): # Perform 100 coin flips.
if random.randint(0, 1) == 0:
print('H', end=' ')
else:
print('T', end=' ')
print() # Print one newline at the end.
这个程序将 H 和 T 的结果显示在一行紧凑的行上,而不是每行一个 H 或 T 结果来展开:
T H T T T H H T T T T H H H H T H H T T T T T H T T T T T H T T T T T H T H T
H H H T T H T T T T H T H H H T H H T H T T T T T H T T H T T T T H T H H H T
T T T H T T T T H H H T H T H H H H T H H T
类似地,当你向print()传递多个字符串值时,该函数会自动用单个空格将它们分隔开来。要查看这种行为,请在交互式外壳中输入以下内容:
>>> print('cats', 'dogs', 'mice')
cats dogs mice
你可以通过传递不同的字符串给sep命名参数来替换默认的分隔字符串。请在交互式外壳中输入以下内容:
>>> print('cats', 'dogs', 'mice', sep=',')
cats,dogs,mice
你也可以向你编写的函数添加命名参数,但首先,你将不得不学习第六章和第七章中的列表和字典数据类型。现在,只需知道一些函数在调用函数时可以指定可选的命名参数。
调用栈
想象一下,你和某人进行了一场蜿蜒的对话。你谈论了你的朋友 Alice,这让你想起了关于你的同事 Bob 的一个故事,但首先你必须解释一些关于你的表亲 Carol 的事情。你完成了关于 Carol 的故事,然后回到谈论 Bob,当你的 Bob 的故事结束时,你回到谈论 Alice。但后来你想起你的兄弟 David,所以你讲了他的故事,然后你回到完成你最初关于 Alice 的故事。你的对话遵循了像图 4-1 所示的 栈 结构。在栈中,项目只从顶部添加或移除,当前主题总是位于栈顶。

图 4-1:您的蜿蜒对话栈描述
就像您的蜿蜒对话一样,调用一个函数并不会将执行发送到函数顶部的单向旅行。Python 会记住调用函数的代码行,以便在遇到 return 语句时返回到那里。如果原始函数调用了其他函数,执行将首先返回到 那些 函数调用,然后再从原始函数调用返回。调用栈顶部的函数调用是当前执行的当前位置。
打开一个文件编辑窗口,输入以下代码,并将其保存为 abcdCallStack.py:
def a():
print('a() starts')
print('Good morning, ' + name) # ❷
print('Good morning, ' + name) # ❷
print('a() returns')
def b():
print('b() starts')
print('Good morning, ' + name) # ❷
print('b() returns')
def c():
print('Good morning, ' + name) # ❷
print('c() returns')
def d():
print('d() starts')
print('d() returns')
a() # ❺
如果你运行这个程序,输出将看起来像这样:
a() starts
b() starts
c() starts
c() returns
b() returns
d() starts
d() returns
a() returns
当 a() 被调用 ❺ 时,它调用 b() ❶,然后 b() 又调用 c() ❸。c() 函数没有调用任何东西;它只是显示 c() starts ❹ 和 c() returns 然后返回到 b() 中调用它的行 ❸。一旦执行返回到 b() 中调用 c() 的代码,它就返回到 a() 中调用 b() 的行 ❶。执行继续到 a() 函数中的下一行 ❷,这是一次对 d() 的调用。就像 c() 函数一样,d() 函数也没有调用任何东西。它只是显示 d() starts 和 d() returns 然后返回到 a() 中调用它的行。因为 d() 不包含其他代码,执行返回到 a() 中调用 d() 的行 ❷。a() 中的最后一行在返回到程序末尾的原始 a() 调用之前显示 a() returns。
调用栈 是 Python 记录每次函数调用后返回执行位置的方式。调用栈不是存储在程序中的变量中;相反,它是计算机内存中由 Python 在幕后自动处理的一个部分。当程序调用一个函数时,Python 在调用栈的顶部创建一个 帧对象。帧对象存储原始函数调用的行号,以便 Python 可以记住返回的位置。如果程序再次调用其他函数,Python 会在调用栈的上方添加另一个帧对象。
当一个函数调用返回时,Python 会从栈顶移除一个帧对象,并将执行移动到该帧对象中存储的行号。请注意,帧对象总是从栈顶添加和移除,而不是从任何其他位置。图 4-2 展示了在 abcdCallStack.py 中每个函数被调用和返回时的调用栈状态。

图 4-2:调用栈的帧对象,当 abcdCallStack.py 从函数调用和返回时的描述
调用栈的顶部是当前正在执行的功能。当调用栈为空时,执行在所有函数之外的一行上。
调用栈是一个技术细节,你不需要严格了解它来编写程序。了解函数调用返回到它们被调用的行号就足够了。然而,了解调用栈使理解下一节中描述的局部和全局作用域变得更容易。
局部和全局作用域
只有在调用函数内部的代码可以访问在该函数中分配的参数和变量。这些变量被称为存在于该函数的 局部作用域 中。相比之下,程序中的任何地方的代码都可以访问在所有函数外部分配的变量。这些变量被称为存在于 全局作用域 中。存在于局部作用域中的变量被称为 局部变量,而存在于全局作用域中的变量被称为 全局变量。一个变量必须是其中之一;它不能同时是局部和全局的。
将 作用域 想象为变量的容器。只有一个全局作用域,在程序开始时创建。当程序结束时,它销毁全局作用域,以及其中的所有变量都被遗忘。每当程序调用一个函数时,就会创建一个新的局部作用域。在函数中分配的任何变量都存在于该函数的局部作用域中。当函数返回时,局部作用域及其变量都会被销毁。
Python 使用作用域是因为它允许一个函数修改其变量,同时仅通过其参数和返回值与程序的其余部分交互。这缩小了可能导致错误的代码行数。如果你的程序中只有全局变量,并且包含由设置错误值的变量引起的错误,你可能很难追踪这个错误值的位置。它可能来自程序中的任何地方,这可能是一百或一千行长!但如果错误发生在局部变量中,你可以将搜索范围限制在单个函数内。
因此,虽然在小程序中使用全局变量是可以的,但随着程序越来越大,依赖全局变量是一个坏习惯。
范围规则
当处理局部和全局变量时,请记住以下规则:
-
全局作用域中,所有函数之外的代码不能使用局部变量。
-
一个函数的局部作用域中的代码不能使用任何其他局部作用域中的变量。
-
局部作用域中的代码可以访问全局变量。
-
如果不同的作用域中有不同的变量名,你可以使用相同的名字。也就是说,可以有一个名为
spam的局部变量和一个也名为spam的全局变量。
让我们通过例子来回顾这些规则。
全局作用域中的代码不能使用局部变量
考虑以下代码,当你运行它时会导致错误:
def spam():
print('Good morning, ' + name) # ❷
spam()
print(eggs)
程序的输出将如下所示:
Traceback (most recent call last):
File "C:/test1.py", line 4, in <module>
print(eggs)
NameError: name 'eggs' is not defined
错误发生是因为 eggs 变量只存在于调用 spam() 时创建的局部作用域中 ❶。一旦程序执行从 spam() 返回,该局部作用域就会被销毁,不再有名为 eggs 的变量。所以当你的程序尝试运行 print(eggs) 时,Python 会给你一个错误,说 eggs 未定义。如果你这样想,这就有意义了;当程序执行在全局作用域中时,没有局部作用域,因此不可能有任何局部变量。这就是为什么你只能在全局作用域中引用全局变量。
局部作用域中的代码不能使用其他局部作用域中的变量
每当程序调用一个函数时,Python 都会创建一个新的局部作用域,即使这个函数是从另一个函数中调用的。考虑以下程序:
def spam():
eggs = 'SPAMSPAM'
print('Good morning, ' + name) # ❷
print('Good morning, ' + name) # ❷
def bacon():
ham = 'hamham'
eggs = 'BACONBACON'
spam() # ❸
当程序开始时,它调用 spam() 函数 ❸,创建一个局部作用域。spam() 函数将局部变量 eggs 设置为 'SPAMSPAM',然后调用 bacon() 函数 ❶,创建第二个局部作用域。可以同时存在多个局部作用域。在这个新的局部作用域中,局部变量 ham 被设置为 'hamham',并创建了一个名为 eggs 的局部变量(与 spam() 的局部作用域中的不同),并将其设置为 'BACONBACON'。此时,程序有两个名为 eggs 的局部变量同时存在:一个属于 spam(),另一个属于 bacon()。
当 bacon() 返回时,Python 会销毁该调用创建的局部作用域,包括其 eggs 变量。程序执行继续在 spam() 函数中,打印 eggs 的值 ❷。因为 spam() 调用的局部作用域仍然存在,唯一的 eggs 变量是 spam() 函数的 eggs 变量,其值被设置为 'SPAMSPAM'。这就是程序打印的内容。
局部作用域中的代码可以使用全局变量
到目前为止,我已经演示了全局作用域中的代码不能访问局部作用域中的变量;同样,不同局部作用域中的代码也不能。现在考虑以下程序:
def spam():
print(eggs) # Prints 'GLOBALGLOBAL'
eggs = 'GLOBALGLOBAL'
spam()
print(eggs)
因为 spam() 函数没有名为 eggs 的参数,也没有任何代码将值赋给 eggs,Python 认为函数对 eggs 的使用是全局变量 eggs 的引用。这就是为什么程序运行时打印出 'GLOBALGLOBAL'。
局部变量和全局变量可以具有相同的名字
从技术上讲,在具有不同作用域的全局变量和局部变量中使用相同的变量名是完全可接受的。但是,为了简化你的生活,避免这样做。为了了解可能会发生什么,将以下代码输入到文件编辑器中,并将其保存为 localGlobalSameName.py:
def spam():
print('Good morning, ' + name) # ❷
print(eggs) # Prints 'spam local'
def bacon():
print('Good morning, ' + name) # ❷
print(eggs) # Prints 'bacon local'
spam()
print(eggs) # Prints 'bacon local'
eggs = 'global' # ❸
bacon()
print(eggs) # Prints 'global'
当你运行这个程序时,它将输出以下内容:
bacon local
spam local
bacon local
global
这个程序实际上包含三个不同的变量,但令人困惑的是,它们都命名为 eggs。变量如下:
-
当调用
spam()时,存在一个名为eggs的局部变量❶ -
当调用
bacon()时,存在一个名为eggs的局部变量❷ -
在全局作用域中存在的一个名为
eggs的变量❸
由于这三个独立的变量都具有相同的名称,所以在任何给定时间跟踪正在使用的变量可能会很困难。相反,即使它们出现在不同的作用域中,也要为所有变量提供唯一的名称。
全局语句
如果你需要在函数内部修改全局变量,请使用 global 语句。在函数顶部包含一行如 global eggs 告诉 Python,“在这个函数中,eggs 指的是全局变量,所以不要用这个名称创建一个局部变量。”例如,将以下代码输入到文件编辑器中,并将其保存为 globalStatement.py:
def spam():
print('Good morning, ' + name) # ❷
print('Good morning, ' + name) # ❷
eggs = 'global'
spam()
print(eggs) # Prints 'spam'
当你运行这个程序时,最后的 print() 调用将输出以下内容:
spam
由于 eggs 在 spam() 的顶部被声明为 global❶,将 eggs 设置为 'spam'❷会改变全局作用域中 eggs 的值。从未创建任何局部 eggs 变量。
范围识别
使用以下四个规则来判断一个变量是否属于局部作用域或全局作用域:
1. 全局作用域中的变量(即所有函数之外)始终是全局变量。
2. 具有 global 语句的函数中的变量始终是该函数中的全局变量。
3. 否则,如果一个函数在赋值语句中使用变量,那么它是一个局部变量。
4. 然而,如果函数使用了一个变量,但从未在赋值语句中使用,那么它是一个全局变量。
为了更好地理解这些规则,这里有一个示例程序。将以下代码输入到文件编辑器中,并将其保存为 sameNameLocalGlobal.py:
def spam():
print('Good morning, ' + name) # ❷
eggs = 'spam' # This is the global variable.
def bacon():
print('Good morning, ' + name) # ❷
def ham():
print('Good morning, ' + name) # ❷
eggs = 'global' # This is the global variable.
spam()
print(eggs)
在 spam() 函数中,eggs 指的是全局 eggs 变量,因为该函数包含一个针对它的 global 语句❶。在 bacon() 中,eggs 是局部变量,因为该函数包含一个针对它的赋值语句❷。在 ham()❸ 中,eggs 是全局变量,因为该函数不包含对该变量的赋值语句或 global 语句。如果你运行 sameNameLocalGlobal.py,输出将如下所示:
spam
如果你试图在给变量赋值之前在函数中使用局部变量,就像以下程序中那样,Python 将会给你一个错误。为了看到这一点,将以下代码输入到文件编辑器中,并将其保存为 sameNameError.py:
def spam():
print(eggs) # ERROR!
print('Good morning, ' + name) # ❷
eggs = 'global' # ❷
spam()
如果你运行前面的程序,它会产生一个错误信息:
Traceback (most recent call last):
File "C:/sameNameError.py", line 6, in <module>
spam()
File "C:/sameNameError.py", line 2, in spam
print(eggs) # ERROR!
UnboundLocalError: local variable 'eggs' referenced before assignment
这个错误发生是因为 Python 看到在 spam() 函数中有 eggs 的赋值语句 ❶,因此认为 spam() 中对 eggs 变量的任何提及都是局部变量。但是因为 print(eggs) 在 eggs 被赋值之前执行,所以局部变量 eggs 不存在。Python 不会回退到使用全局的 eggs 变量 ❷。
错误名称 UnboundLocalError 可能有些令人困惑。在 Python 中,绑定 另一种说法是 赋值,所以这个错误表明程序在未赋值之前使用了局部变量。
作用域规则
当处理局部和全局变量时,请记住以下规则:
-
位于全局作用域(所有函数外部)的代码不能使用局部变量。
-
位于一个函数的局部作用域中的代码不能使用任何其他局部作用域中的变量。
-
位于局部作用域中的代码可以访问全局变量。
-
如果不同的作用域中有不同的变量,你可以使用相同的名称。也就是说,可以有一个名为
spam的局部变量和一个名为spam的全局变量。
让我们通过示例来回顾这些规则。
全局作用域中的代码不能使用局部变量
考虑以下代码,当你运行它时会导致错误:
def spam():
print('Good morning, ' + name) # ❷
spam()
print(eggs)
程序的输出将如下所示:
Traceback (most recent call last):
File "C:/test1.py", line 4, in <module>
print(eggs)
NameError: name 'eggs' is not defined
错误发生是因为当调用 spam() 函数时,eggs 变量只存在于由 spam() 创建的局部作用域中 ❶。一旦程序从 spam() 返回,该局部作用域就会被销毁,并且不再存在名为 eggs 的变量。所以当你的程序尝试运行 print(eggs) 时,Python 会给你一个错误,说 eggs 未定义。如果你这样想的话,这就有道理了;当程序执行在全局作用域时,没有局部作用域存在,因此不可能有任何局部变量。这就是为什么你只能在全局作用域中引用全局变量。
位于局部作用域中的代码不能使用其他局部作用域中的变量。
每当程序调用一个函数时,Python 都会创建一个新的局部作用域,即使该函数是从另一个函数中调用的。考虑以下程序:
def spam():
eggs = 'SPAMSPAM'
print('Good morning, ' + name) # ❷
print('Good morning, ' + name) # ❷
def bacon():
ham = 'hamham'
eggs = 'BACONBACON'
spam() # ❸
当程序开始时,它会调用 spam() 函数 ❸,创建一个局部作用域。spam() 函数将局部变量 eggs 设置为 'SPAMSPAM',然后调用 bacon() 函数 ❶,创建第二个局部作用域。可以同时存在多个局部作用域。在这个新的局部作用域中,局部变量 ham 被设置为 'hamham',并且创建了一个名为 eggs 的局部变量(它与 spam() 的局部作用域中的不同),并将其设置为 'BACONBACON'。此时,程序有两个名为 eggs 的局部变量同时存在:一个属于 spam(),另一个属于 bacon()。
当bacon()返回时,Python 会销毁该调用所创建的局部作用域,包括其eggs变量。程序执行继续在spam()函数中,打印eggs的值 ❷。因为对spam()的调用仍然存在局部作用域,所以唯一的eggs变量是spam()函数的eggs变量,其值被设置为'SPAMSPAM'。这就是程序打印的内容。
局部作用域中的代码可以使用全局变量
到目前为止,我已经演示了全局作用域中的代码不能访问局部作用域中的变量;同样,不同局部作用域中的代码也不能。现在考虑以下程序:
def spam():
print(eggs) # Prints 'GLOBALGLOBAL'
eggs = 'GLOBALGLOBAL'
spam()
print(eggs)
因为spam()函数没有名为eggs的参数,也没有将eggs赋值的代码,Python 认为函数对eggs的使用是对全局变量eggs的引用。这就是为什么程序运行时打印出'GLOBALGLOBAL'。
局部和全局变量可以具有相同的名称
从技术上讲,在全局变量和不同作用域中的局部变量中使用相同的变量名是完全可接受的。但是,为了简化你的生活,避免这样做。为了看看可能会发生什么,将以下代码输入到文件编辑器中,并将其保存为localGlobalSameName.py:
def spam():
print('Good morning, ' + name) # ❷
print(eggs) # Prints 'spam local'
def bacon():
print('Good morning, ' + name) # ❷
print(eggs) # Prints 'bacon local'
spam()
print(eggs) # Prints 'bacon local'
eggs = 'global' # ❸
bacon()
print(eggs) # Prints 'global'
当你运行这个程序时,它将输出以下内容:
bacon local
spam local
bacon local
global
这个程序实际上包含三个不同的变量,但令人困惑的是,它们都命名为eggs。变量如下:
-
当调用
spam()时存在于局部作用域中的名为eggs的变量 ❶ -
当调用
bacon()时存在于局部作用域中的名为eggs的变量 ❷ -
存在于全局作用域中的名为
eggs的变量 ❸
因为这三个独立的变量都具有相同的名称,所以在任何给定时间跟踪正在使用的变量可能会很困难。相反,即使它们出现在不同的作用域中,也要给所有变量赋予唯一的名称。
全局作用域中的代码不能使用局部变量
考虑以下代码,当你运行它时将会引发错误:
def spam():
print('Good morning, ' + name) # ❷
spam()
print(eggs)
程序的输出将如下所示:
Traceback (most recent call last):
File "C:/test1.py", line 4, in <module>
print(eggs)
NameError: name 'eggs' is not defined
错误发生是因为eggs变量仅存在于调用spam()时创建的局部作用域中 ❶。一旦程序从spam()返回,该局部作用域就会被销毁,并且不再存在名为eggs的变量。所以当你的程序尝试运行print(eggs)时,Python 会给你一个错误,说eggs未定义。如果你这样想,这就有道理了;当程序执行处于全局作用域时,没有局部作用域,因此不可能有任何局部变量。这就是为什么你只能在全局作用域中引用全局变量。
局部作用域中的代码不能使用其他局部作用域中的变量
每当程序调用一个函数时,Python 都会创建一个新的局部作用域,即使这个函数是从另一个函数中调用的。考虑以下程序:
def spam():
eggs = 'SPAMSPAM'
print('Good morning, ' + name) # ❷
print('Good morning, ' + name) # ❷
def bacon():
ham = 'hamham'
eggs = 'BACONBACON'
spam() # ❸
当程序开始运行时,它会调用 spam() 函数 ❸,创建一个局部作用域。spam() 函数将局部变量 eggs 设置为 'SPAMSPAM',然后调用 bacon() 函数 ❶,创建第二个局部作用域。可以同时存在多个局部作用域。在这个新的局部作用域中,局部变量 ham 被设置为 'hamham',并创建了一个名为 eggs 的局部变量(它与 spam() 的局部作用域中的不同),并将其设置为 'BACONBACON'。此时,程序有两个名为 eggs 的局部变量同时存在:一个属于 spam(),另一个属于 bacon()。
当 bacon() 返回时,Python 会销毁该调用对应的局部作用域,包括其 eggs 变量。程序执行继续在 spam() 函数中,打印 eggs 的值 ❷。因为 spam() 调用的局部作用域仍然存在,所以唯一的 eggs 变量是 spam() 函数的 eggs 变量,它被设置为 'SPAMSPAM'。这就是程序打印的内容。
局部作用域中的代码可以使用全局变量
到目前为止,我已经演示了全局作用域中的代码无法访问局部作用域中的变量;同样,不同局部作用域中的代码也无法相互访问。现在考虑以下程序:
def spam():
print(eggs) # Prints 'GLOBALGLOBAL'
eggs = 'GLOBALGLOBAL'
spam()
print(eggs)
因为 spam() 函数没有名为 eggs 的参数,也没有给 eggs 赋值的代码,Python 将函数对 eggs 的使用视为对全局变量 eggs 的引用。这就是为什么程序运行时打印出 'GLOBALGLOBAL'。
局部和全局变量可以具有相同的名称
从技术上讲,在具有不同作用域的全局变量和局部变量中使用相同的变量名是完全可接受的。但是,为了简化你的生活,避免这样做。为了了解可能会发生什么,将以下代码输入到文件编辑器中,并将其保存为 localGlobalSameName.py:
def spam():
print('Good morning, ' + name) # ❷
print(eggs) # Prints 'spam local'
def bacon():
print('Good morning, ' + name) # ❷
print(eggs) # Prints 'bacon local'
spam()
print(eggs) # Prints 'bacon local'
eggs = 'global' # ❸
bacon()
print(eggs) # Prints 'global'
当你运行这个程序时,它会输出以下内容:
bacon local
spam local
bacon local
global
这个程序实际上包含三个不同的变量,但令人困惑的是,它们都命名为 eggs。变量如下:
-
当调用
spam()函数时存在的局部作用域中的名为eggs的变量 ❶ -
当调用
bacon()函数时存在的局部作用域中的名为eggs的变量 ❷ -
存在于全局作用域中的名为
eggs的变量 ❸
因为这三个不同的变量都有相同的名称,所以在任何给定时间跟踪正在使用的变量可能会很困难。相反,即使它们出现在不同的作用域中,也要给所有变量赋予唯一的名称。
全局声明
如果需要在函数内部修改全局变量,请使用 global 语句。在函数顶部包含一行如 global eggs 的代码告诉 Python,“在这个函数中,eggs 指的是全局变量,所以不要创建一个具有此名称的局部变量。”例如,将以下代码输入到文件编辑器中,并将其保存为 globalStatement.py:
def spam():
print('Good morning, ' + name) # ❷
print('Good morning, ' + name) # ❷
eggs = 'global'
spam()
print(eggs) # Prints 'spam'
当你运行这个程序时,最后的 print() 调用将输出以下内容:
spam
因为 eggs 在 spam() 函数的顶部被声明为 global ❶,所以将 eggs 设置为 'spam'❷ 会改变全局作用域中 eggs 的值。永远不会创建任何局部 eggs 变量。
范围识别
使用这四个规则来判断一个变量属于局部作用域还是全局作用域:
-
在全局作用域(即所有函数外部)中的变量始终是全局变量。
-
在包含
global语句的函数中的变量始终是该函数中的全局变量。 -
否则,如果一个函数在赋值语句中使用了一个变量,那么它就是一个局部变量。
-
然而,如果一个函数使用了某个变量,但从未在赋值语句中使用,那么它就是一个全局变量。
为了更好地理解这些规则,这里有一个示例程序。将以下代码输入到文件编辑器中,并将其保存为 sameNameLocalGlobal.py:
def spam():
print('Good morning, ' + name) # ❷
eggs = 'spam' # This is the global variable.
def bacon():
print('Good morning, ' + name) # ❷
def ham():
print('Good morning, ' + name) # ❷
eggs = 'global' # This is the global variable.
spam()
print(eggs)
在 spam() 函数中,eggs 指的是全局 eggs 变量,因为该函数包含一个针对它的 global 语句 ❶。在 bacon() 中,eggs 是局部变量,因为该函数包含一个针对它的赋值语句 ❷。在 ham() ❸ 中,eggs 是全局变量,因为该函数不包含该变量的赋值语句或 global 语句。如果你运行 sameNameLocalGlobal.py,输出将如下所示:
spam
如果你尝试在函数中在为其赋值之前使用局部变量,就像以下程序中那样,Python 会给你一个错误。要查看这一点,将以下代码输入到文件编辑器中,并将其保存为 sameNameError.py:
def spam():
print(eggs) # ERROR!
print('Good morning, ' + name) # ❷
eggs = 'global' # ❷
spam()
如果你运行前面的程序,它会生成一个错误消息:
Traceback (most recent call last):
File "C:/sameNameError.py", line 6, in <module>
spam()
File "C:/sameNameError.py", line 2, in spam
print(eggs) # ERROR!
UnboundLocalError: local variable 'eggs' referenced before assignment
这个错误发生是因为 Python 看到在 spam() 函数中有一个针对 eggs 的赋值语句 ❶,因此认为 spam() 中对 eggs 变量的任何提及都是局部变量。但是因为 print(eggs) 在 eggs 被赋值之前执行,所以局部变量 eggs 不存在。Python 不会回退到使用全局 eggs 变量 ❷。
错误名称 UnboundLocalError 可能有些令人困惑。在 Python 中,绑定 是另一种说法,即 赋值,所以这个错误表明程序在未赋值之前使用了局部变量。
异常处理
目前,在 Python 程序中遇到错误或 异常 会导致整个程序崩溃。你不想在现实世界的程序中发生这种情况。相反,你希望程序能够检测错误,处理它们,然后继续运行。
例如,考虑以下程序,它有一个除以零的错误。打开文件编辑器窗口,输入以下代码,并将其保存为 zeroDivide.py:
def spam(divide_by):
return 42 / divide_by
print(spam(2))
print(spam(12))
print(spam(0))
print(spam(1))
我们定义了一个名为 spam 的函数,给它一个参数,然后使用不同的参数打印该函数的值,以查看会发生什么。这是运行上一段代码时的输出:
21.0
3.5
Traceback (most recent call last):
File "C:/zeroDivide.py", line 6, in <module>
print(spam(0))
File "C:/zeroDivide.py", line 2, in spam
return 42 / divide_by
ZeroDivisionError: division by zero
每当你尝试将一个数字除以零时,都会发生 ZeroDivisionError。从错误信息中给出的行号,你可以知道 spam() 函数中的 return 语句导致了错误。
大多数情况下,异常表明你的代码中存在需要修复的错误。但有时异常是可以预期的,并且可以从中恢复。例如,在第十章中,你将学习如何从文件中读取文本。如果你为不存在的文件指定了文件名,Python 会引发 FileNotFoundError 异常。你可能想通过要求用户再次输入文件名来处理这个异常,而不是让这个未处理的异常立即崩溃你的程序。
可以使用 try 和 except 语句来处理错误。可能发生错误的代码放在 try 子句中。如果发生错误,程序执行将移动到后续的 except 子句的开始。
你可以将之前的除以零代码放入 try 子句中,并在 except 子句中包含处理此错误发生时发生情况的代码:
def spam(divide_by):
try:
# Any code in this block that causes ZeroDivisionError won't crash the program:
return 42 / divide_by
except ZeroDivisionError:
# If ZeroDivisionError happened, the code in this block runs:
print('Error: Invalid argument.')
print(spam(2))
print(spam(12))
print(spam(0))
print(spam(1))
当 try 子句中的代码导致错误时,程序执行将立即移动到 except 子句中的代码。运行该代码后,执行将继续正常进行。如果程序在 try 子句中没有引发异常,程序将跳过 except 子句中的代码。上一个程序的输出如下:
21.0
3.5
Error: Invalid argument.
None
42.0
注意,try 块中的函数调用中发生的任何错误也会被捕获。考虑以下程序,它将 spam() 调用放在 try 块中:
def spam(divide_by):
return 42 / divide_by
try:
print(spam(2))
print(spam(12))
print(spam(0))
print(spam(1))
except ZeroDivisionError:
print('Error: Invalid argument.')
当这个程序运行时,输出看起来像这样:
21.0
3.5
Error: Invalid argument.
print(spam(1)) 永远不会被执行,因为一旦执行跳转到 except 子句中的代码,它就不会返回到 try 子句。相反,它只是像正常一样继续向下移动程序。
短程序:Z 字形
让我们使用到目前为止学到的编程概念来创建一个小型动画程序。这个程序将创建一个来回的 Z 字形图案,直到用户通过按下 Mu 编辑器的停止按钮或按下 CTRL-C 来停止它。当你运行这个程序时,输出将类似于以下这样:
**
**
**
**
**
**
**
**
**
将以下源代码输入到文件编辑器中,并将文件保存为 zigzag.py:
import time, sys
indent = 0 # How many spaces to indent
indent_increasing = True # Whether the indentation is increasing or not
try:
while True: # The main program loop
print(' ' * indent, end='')
print('**')
time.sleep(0.1) # Pause for 1/10th of a second.
if indent_increasing:
# Increase the number of spaces:
indent = indent + 1
if indent == 20:
# Change direction:
indent_increasing = False
else:
# Decrease the number of spaces:
indent = indent - 1
if indent == 0:
# Change direction:
indent_increasing = True
except KeyboardInterrupt:
sys.exit()
让我们逐行查看这段代码,从顶部开始:
import time, sys
indent = 0 # How many spaces to indent
indent_increasing = True # Whether the indentation is increasing or not
首先,我们将导入 time 和 sys 模块。我们的程序使用两个变量。indent 变量跟踪在八个星号带之前的缩进空格数,而 indent_increasing 变量包含一个布尔值,用于确定缩进量是增加还是减少:
try:
while True: # The main program loop
print(' ' * indent, end='')
print('**')
time.sleep(0.1) # Pause for 1/10 of a second.
接下来,我们将程序的其余部分放入一个 try 语句中。当用户在运行 Python 程序时按下 CTRL-C,Python 会引发 KeyboardInterrupt 异常。如果没有 try-except 语句来捕获这个异常,程序会因一个难看的错误信息而崩溃。然而,我们希望我们的程序通过调用 sys.exit() 来干净地处理 KeyboardInterrupt 异常。(您可以在程序末尾的 except 语句中找到实现这一点的代码。)
while True: 无限循环将永远重复程序中的指令。这涉及到使用 ' ' * indent 来打印正确的缩进空格数。我们不想在这些空格后自动打印换行符,所以我们还向第一个 print() 调用传递 end=''。第二个 print() 调用打印星号带。我们还没有讨论 time.sleep() 函数;简单地说,它在我们程序中引入了十分之一秒的暂停:
if indent_increasing:
# Increase the number of spaces:
indent = indent + 1
if indent == 20:
indent_increasing = False # Change direction
接下来,我们想要调整下一次打印星号时使用的缩进量。如果 indent_increasing 为 True,我们将向 indent 中添加 1,但一旦缩进达到 20,我们将减少缩进:
else:
# Decrease the number of spaces:
indent = indent - 1
if indent == 0:
indent_increasing = True # Change direction
如果 indent_increasing 为 False,我们将从 indent 中减去 1。一旦缩进达到 0,我们希望缩进再次增加。无论如何,程序执行将跳回主程序循环的开始,再次打印星号。
如果用户在程序执行处于 try 块中的任何时刻按下 CTRL-C,这个 except 语句将引发并处理 KeyboardInterrupt 异常:
except KeyboardInterrupt:
sys.exit()
程序执行将进入 except 块,它运行 sys.exit() 并退出程序。这样,尽管主程序循环是无限的,但用户仍然有办法关闭程序。
简短程序:尖峰
让我们再创建一个滚动动画程序。这个程序使用字符串复制和嵌套循环来绘制尖峰。在你的代码编辑器中打开一个新文件,输入以下代码,并将其保存为 spike.py:
import time, sys
try:
while True: # The main program loop
# Draw lines with increasing length:
for i in range(1, 9):
print('-' * (i * i))
time.sleep(0.1)
# Draw lines with decreasing length:
for i in range(7, 1, -1):
print('-' * (i * i))
time.sleep(0.1)
except KeyboardInterrupt:
sys.exit()
当你运行这个程序时,它会反复绘制以下尖峰:
-
----
---------
----------------
-------------------------
------------------------------------
-------------------------------------------------
----------------------------------------------------------------
-------------------------------------------------
------------------------------------
-------------------------
----------------
---------
----
与之前的锯齿形程序一样,尖峰程序需要调用 time.sleep() 和 sys.exit() 函数。第一行导入 time 和 sys 模块。一个 try 块将捕获用户按下 CTRL-C 时引发的 KeyboardInterrupt 异常,并且一个无限循环将永远继续绘制尖峰。
第一个 for 循环绘制了逐渐增大的尖峰:
# Draw lines with increasing length:
for i in range(1, 9):
print('-' * (i * i))
time.sleep(0.1)
因为 i 变量被设置为 1,然后 2,然后 3,以此类推,直到但不包括 9,下面的 print() 调用通过 1 * 1(即 1)、然后 2 * 2(即 4)、然后 3 * 3(即 9)等来复制 '-' 字符串。这段代码创建了长度为 1、4、9、16、25、36、49 的字符串,然后是 64 个连字符长。通过创建指数级增长的长字符串,我们创建了尖峰的上半部分。
要绘制尖峰的下半部分,我们需要另一个 for 循环,使 i 从 7 开始然后递减到 1,不包括 1:
# Draw lines with decreasing length:
for i in range(7, 1, -1):
print('-' * (i * i))
time.sleep(0.1)
如果你想要改变尖峰的宽度,你可以修改两个 for 循环中的 9 和 7 的值。其余的代码将使用这些新值正常工作。
概述
函数是将你的代码划分为逻辑组的主要方式。由于函数中的变量存在于它们自己的局部作用域中,一个函数中的代码不能直接影响其他函数中局部变量的值。这限制了可以更改你的变量值的代码部分,这在调试时可能很有帮助。
函数是帮助你组织代码的伟大工具。你可以把它们想象成黑盒:它们有参数形式的输入和返回值形式的输出,它们中的代码不会影响其他函数中的变量。
在前面的章节中,一个错误可能导致你的程序崩溃。在本章中,你学习了 try 和 except 语句,当检测到错误时可以运行代码。这可以使你的程序对常见的错误情况更具弹性。
实践问题
-
为什么在你的程序中拥有函数是有优势的?
-
函数中的代码何时执行:是在函数定义时还是函数被调用时?
-
哪个语句创建一个函数?
-
函数和函数调用之间的区别是什么?
-
Python 程序中有多少个全局作用域?有多少个局部作用域?
-
当函数调用返回时,局部作用域中的变量会发生什么?
-
返回值是什么?返回值可以是表达式的一部分吗?
-
如果一个函数没有
return语句,对该函数的调用返回什么值? -
你如何强制函数中的变量引用全局变量?
-
None的数据类型是什么? -
import areallyourpetsnamederic语句的作用是什么? -
如果你有一个名为
bacon()的函数在一个名为spam的模块中,导入spam后你将如何调用它? -
你如何防止程序在出现错误时崩溃?
-
try子句中应该放什么?except子句中应该放什么? -
将以下程序写入名为 notrandomdice.py 的文件中并运行它。为什么每个函数调用都返回相同的数字?
import random
random_number = random.randint(1, 6)
def get_random_dice_roll():
# Returns a random integer from 1 to 6
return random_number
print(get_random_dice_roll())
print(get_random_dice_roll())
print(get_random_dice_roll())
print(get_random_dice_roll())
实践程序
为了练习,编写程序来完成以下任务。
Collatz 序列
编写一个名为 collatz() 的函数,它有一个名为 number 的参数。如果 number 是偶数,则 collatz() 应该打印 number // 2 并返回此值。如果 number 是奇数,则 collatz() 应该打印并返回 3 * number + 1。
然后,编写一个程序,允许用户输入一个整数,并持续调用collatz()函数,直到该函数返回值1。 (令人惊讶的是,这个序列实际上适用于任何整数;迟早,使用这个序列,你会到达 1!甚至数学家也不确定为什么。你的程序正在探索所谓的Collatz 序列,有时被称为“最简单的不可能的数学问题。”)
记住使用int()函数将input()的返回值转换为整数;否则,它将是一个字符串值。为了使输出更紧凑,打印数字的print()调用应该有一个名为sep=' '的命名参数,以便在一行上打印所有值。
这个程序的输出可能看起来像这样:
Enter number:
3
3 10 5 16 8 4 2 1
提示:一个整数number如果是偶数,则number % 2等于0,如果是奇数,则number % 2等于1。
输入验证
在上一个项目中添加try和except语句以检测用户是否输入了非整数字符串。通常,如果int()函数传递了非整数字符串,如int('puppy'),它将引发ValueError错误。在except子句中,向用户打印一条消息,说明他们必须输入一个整数。
Collatz 序列
编写一个名为collatz()的函数,它有一个名为number的参数。如果number是偶数,则collatz()应该打印number // 2并返回此值。如果number是奇数,则collatz()应该打印并返回3 * number + 1。
然后,编写一个程序,允许用户输入一个整数,并持续调用collatz()函数,直到该函数返回值1。 (令人惊讶的是,这个序列实际上适用于任何整数;迟早,使用这个序列,你会到达 1!甚至数学家也不确定为什么。你的程序正在探索所谓的Collatz 序列,有时被称为“最简单的不可能的数学问题。”)
记住使用int()函数将input()的返回值转换为整数;否则,它将是一个字符串值。为了使输出更紧凑,打印数字的print()调用应该有一个名为sep=' '的命名参数,以便在一行上打印所有值。
这个程序的输出可能看起来像这样:
Enter number:
3
3 10 5 16 8 4 2 1
提示:一个整数number如果是偶数,则number % 2等于0,如果是奇数,则number % 2等于1。
输入验证
在上一个项目中添加try和except语句以检测用户是否输入了非整数字符串。通常,如果int()函数传递了非整数字符串,如int('puppy'),它将引发ValueError错误。在except子句中,向用户打印一条消息,说明他们必须输入一个整数。


浙公网安备 33010602011771号