原文:
inventwithpython.com/invent4thed/chapter5.html
译者:飞龙
本章中您将创建的游戏名为龙之境。玩家需要在两个洞穴之间做出选择,这两个洞穴分别藏有宝藏和一定的厄运。
在这个游戏中,玩家身处一个充满龙的土地。这些龙都住在洞穴里,洞穴里堆满了它们收集的宝藏。一些龙是友好的,会分享它们的宝藏。其他龙是饥饿的,会吃掉进入它们洞穴的任何人。玩家走近两个洞穴,一个有友好的龙,另一个有饥饿的龙,但不知道哪个洞穴里有哪种龙。玩家必须在两者之间做出选择。
本章涵盖的主题
流程图
使用def
关键字创建您自己的函数
多行字符串
while
语句
and
、or
和not
布尔运算符
真值表
return
关键字
全局和局部变量范围
参数和参数
sleep()
函数
当运行 Dragon Realm 游戏时,游戏看起来是这样的。玩家的输入是用粗体标出的。
You are in a land full of dragons. In front of you,
you see two caves. In one cave, the dragon is friendly
and will share his treasure with you. The other dragon
is greedy and hungry, and will eat you on sight.
Which cave will you go into? (1 or 2)
1
You approach the cave...
It is dark and spooky...
A large dragon jumps out in front of you! He opens his jaws and...
Gobbles you down in one bite!
Do you want to play again? (yes or no)
no
在开始编写代码之前,将您的游戏或程序需要做的一切都写下来通常会有所帮助。这样做时,您正在设计程序。
例如,绘制一个流程图可能会有所帮助。流程图是一种图表,显示了游戏中可能发生的每一个动作以及这些动作是如何连接的。图 5-1 是龙之境的流程图。
要查看游戏中发生的情况,请将手指放在“开始”框上。然后从该框中的一个箭头指向另一个框。您的手指就像程序执行一样。当您的手指落在“结束”框上时,程序终止。
图 5-1:龙之境游戏的流程图
当您到达“检查友好或饥饿的龙”框时,您可以进入“玩家获胜”框或“玩家失败”框。在这个分支点上,程序可以朝不同的方向发展。无论如何,这两条路径最终都会到达“询问是否再玩一次”框。
通过点击文件新建窗口打开一个新的文件编辑窗口。输入源代码并将其保存为dragon.py。然后按 F5 运行程序。如果出现错误,请使用在线 diff 工具比较您输入的代码与书中的代码,网址为www.nostarch.com/inventwithpython#diff
。
dragon.py
import random
import time
def displayIntro():
print('''You are in a land full of dragons. In front of you,
you see two caves. In one cave, the dragon is friendly
and will share his treasure with you. The other dragon
is greedy and hungry, and will eat you on sight.''')
print()
def chooseCave():
cave = ''
while cave != '1' and cave != '2':
print('Which cave will you go into? (1 or 2)')
cave = input()
return cave
def checkCave(chosenCave):
print('You approach the cave...')
time.sleep(2)
print('It is dark and spooky...')
time.sleep(2)
print('A large dragon jumps out in front of you! He opens his jaws
and...')
print()
time.sleep(2)
friendlyCave = random.randint(1, 2)
if chosenCave == str(friendlyCave):
print('Gives you his treasure!')
else:
print('Gobbles you down in one bite!')
playAgain = 'yes'
while playAgain == 'yes' or playAgain == 'y':
displayIntro()
caveNumber = chooseCave()
checkCave(caveNumber)
print('Do you want to play again? (yes or no)')
playAgain = input()
让我们更详细地看一下源代码。
该程序导入了两个模块:
import random
import time
random
模块提供了randint()
函数,我们在第 3 章的猜数字游戏中使用了这个函数。第 2 行导入了time
模块,其中包含与时间相关的函数。
函数让您可以多次运行相同的代码,而无需一遍又一遍地复制和粘贴该代码。相反,您将该代码放在一个函数中,并在需要时调用该函数。因为您只需在函数中编写一次代码,所以如果函数的代码出现错误,您只需在程序中的一个地方进行更改。
您已经使用了一些函数,比如print()
、input()
、randint()
、str()
和int()
。您的程序已经调用这些函数来执行其中的代码。在 Dragon Realm 游戏中,您将使用def
语句编写自己的函数。
第 4 行是一个def
语句:
def displayIntro():
print('''You are in a land full of dragons. In front of you,
you see two caves. In one cave, the dragon is friendly
and will share his treasure with you. The other dragon
is greedy and hungry, and will eat you on sight.''')
print()
def
语句定义了一个新的函数(在这种情况下是displayIntro()
函数),你可以在程序的其他地方调用它。
图 5-2 显示了def
语句的部分。它有def
关键字,后面跟着一个带括号的函数名,然后是一个冒号(:
)。def
语句之后的块称为def
块。
图 5-2: def 语句的部分
当你定义一个函数时,你在接下来的块中指定它运行的指令。当你调用一个函数时,def
块中的代码会执行。除非你调用这个函数,def
块中的指令不会执行。
换句话说,当执行到def
语句时,它会跳到def
块之后的第一行。但是当一个函数被调用时,执行会移动到函数内部的def
块的第一行。
例如,看看第 37 行对displayIntro()
函数的调用:
displayIntro()
调用这个函数会运行print()
调用,显示“你现在处于一个充满龙的土地…”的介绍。
函数的def
语句和def
块必须在调用函数之前,就像在使用变量之前必须为变量赋值一样。如果你把函数调用放在函数定义之前,你会得到一个错误。让我们看一个短程序作为例子。打开一个新的文件编辑窗口,输入这段代码,保存为example.py,然后运行它:
sayGoodbye()
def sayGoodbye():
print('Goodbye!')
如果你尝试运行这个程序,Python 会给你一个错误消息,看起来像这样:
Traceback (most recent call last):
File "C:/Users/Al/AppData/Local/Programs/Python/Python36/example.py",
line 1, in <module>
sayGoodbye()
NameError: name 'sayGoodbye' is not defined
要修复这个错误,把函数定义移到函数调用之前。
def sayGoodbye():
print('Goodbye!')
sayGoodbye()
现在,函数在被调用之前被定义,所以 Python 会知道sayGoodbye()
应该做什么。否则,当它被调用时,Python 不会有sayGoodbye()
的指令,因此无法运行它。
到目前为止,我们在print()
函数调用中的所有字符串都在一行上,并且在开头和结尾都有一个引号字符。然而,如果你在字符串的开头和结尾使用三个引号,它可以跨越多行。这就是多行字符串。
输入以下内容到交互式 shell 中,看看多行字符串是如何工作的:
>>> fizz = '''Dear Alice,
I will return to Carol's house at the end of the month.
Your friend,
Bob'''
>>> print(fizz)
Dear Alice,
I will return to Carol's house at the end of the month.
Your friend,
Bob
注意打印字符串中的换行符。在多行字符串中,换行符被包含在字符串中。只要不连续使用三个引号,你就不必使用\n
转义字符或转义引号。这些换行符使得在涉及大量文本时代码更容易阅读。
第 11 行定义了另一个名为chooseCave()
的函数:
def chooseCave():
这个函数的代码询问玩家想要进入哪个洞穴,要么是1
要么是2
。我们需要使用while
语句来询问玩家选择洞穴,这标志着一个新类型的循环的开始:while
循环。
不像for
循环会循环特定次数,while
循环会重复直到某个条件为True
。当执行到while
语句时,它会评估while
关键字旁边的条件。如果条件求值为True
,执行会移动到接下来的块,称为while
块。如果条件求值为False
,执行会跳过while
块。
你可以把while
语句看作几乎和if
语句一样。如果它们的条件为True
,程序执行会进入这两个语句的块。但是当while
循环的条件到达块的末尾时,它会回到while
语句重新检查条件。
看看chooseCave()
的def
块,看看while
循环是如何工作的:
cave = ''
while cave != '1' and cave != '2':
第 12 行创建了一个名为cave
的新变量,并在其中存储了一个空字符串。然后在第 13 行开始了一个while
循环。chooseCave()
函数需要确保玩家输入了1
或2
而不是其他内容。在这里的循环中,一直询问玩家选择哪个洞,直到他们输入了这两个有效的回答之一。这被称为输入验证。
条件中还包含了一个你以前没有见过的新运算符:and
。就像-
和*
是数学运算符,==
和!=
是比较运算符一样,and
运算符是布尔运算符。让我们更仔细地看看布尔运算符。
布尔逻辑处理的是真或假的事物。布尔运算符比较值并求值为单个布尔值。
想象一下这句话:“猫有胡须,狗有尾巴。”“猫有胡须”是真的,“狗有尾巴”也是真的,所以整个句子“猫有胡须和狗有尾巴”是真的。
但是句子“猫有胡须,狗有翅膀”就是假的。尽管“猫有胡须”是真的,狗没有翅膀,所以“狗有翅膀”是假的。在布尔逻辑中,事物只能是完全真或完全假。因为有and这个词,整个句子只有两个部分都是真的时才是真的。如果一个或两个部分是假的,那么整个句子就是假的。
Python 中的and
运算符也需要整个布尔表达式为True
或False
。如果and
关键字两侧的布尔值都为True
,那么表达式求值为True
。如果两侧的布尔值中有一个或两个为False
,那么表达式求值为False
。
在交互式 shell 中输入以下带有and
运算符的表达式:
>>> True and True
True
>>> True and False
False
>>> False and True
False
>>> False and False
False
>>> spam = 'Hello'
>>> 10 < 20 and spam == 'Hello'
True
and
运算符可以用来评估任何两个布尔表达式。在最后一个例子中,10 < 20
求值为True
,spam == 'Hello'
也求值为True
,所以被and
运算符连接的两个布尔表达式求值为True
。
如果你忘记了布尔运算符的工作原理,你可以查看它的真值表,它显示了每种布尔值组合的评估方式。表 5-1 显示了and
运算符的每种组合。
表 5-1: and 运算符的真值表
A and B | 求值为 |
---|---|
True and True | True |
True and False | False |
False and True | False |
False and False | False |
or
运算符类似于and
运算符,只是如果两个布尔值中任一个为True
,它就求值为True
。or
运算符求值为False
的唯一情况是如果两个布尔值都为False
。
现在在交互式 shell 中输入以下内容:
>>> True or True
True
>>> True or False
True
>>> False or True
True
>>> False or False
False
>>> 10 > 20 or 20 > 10
True
在最后一个例子中,10
不大于20
,但20
大于10
,所以第一个表达式求值为False
,第二个表达式求值为True
。因为第二个表达式是True
,所以整个表达式求值为True
。
or
运算符的真值表显示在表 5-2 中。
表 5-2: or 运算符的真值表
A or B | 求值为 |
---|---|
True or True | True |
True or False | True |
False or True | True |
False or False | False |
而不是结合两个值,not
运算符只作用于一个值。not
运算符的结果是相反的布尔值:True
表达式求值为False
,False
表达式求值为True
。
在交互式 shell 中输入以下内容:
>>> not True
False
>>> not False
True
>>> not ('black' == 'white')
True
not
运算符也可以用于任何布尔表达式。在最后一个例子中,表达式'black' == 'white'
求值为False
。这就是为什么not ('black' == 'white')
是True
。
not
运算符的真值表显示在表 5-3 中。
表 5-3: not 运算符的真值表
not A | 求值为 |
---|---|
not True | False |
not False | True |
再次看一下 Dragon Realm 游戏的第 13 行:
while cave != '1' and cave != '2':
条件由and
布尔运算符连接的两部分。只有当两部分都为True
时,条件才为True
。
第一次检查while
语句的条件时,cave
被设置为空字符串''
。空字符串不等于字符串'1'
,所以左侧求值为True
。空字符串也不等于字符串'2'
,所以右侧求值为True
。
因此条件变成了True and True
。因为两个值都为True
,整个条件求值为True
,所以程序执行进入while
块,程序将尝试为cave
分配一个非空值。
第 14 行要求玩家选择一个洞穴:
while cave != '1' and cave != '2':
print('Which cave will you go into? (1 or 2)')
cave = input()
第 15 行让玩家输入他们的响应并按 ENTER 键。这个响应存储在cave
中。执行此代码后,执行将循环回到while
语句的顶部,并重新检查第 13 行的条件。
如果玩家输入了1
或2
,那么cave
将是'1'
或'2'
(因为input()
始终返回字符串)。这使得条件为False
,程序执行将继续执行while
循环之后的代码。例如,如果用户输入了'1'
,那么评估将如下所示:
但如果玩家输入了3
或4
或HELLO
,那么该响应将是无效的。条件将为True
,并进入while
块再次询问玩家。程序会一直询问玩家选择哪个洞穴,直到他们输入1
或2
为止。这确保一旦执行继续,cave
变量包含有效的响应。
第 17 行有一个新的return
语句:
return cave
return
语句只出现在def
块内,其中定义了一个函数——在本例中是chooseCave()
。记得input()
函数返回玩家输入的字符串值吗?chooseCave()
函数也会返回一个值。第 17 行返回存储在cave
中的字符串,要么是'1'
,要么是'2'
。
一旦return
语句执行,程序执行立即跳出def
块(就像break
语句使执行跳出while
块一样)。程序执行将返回到具有函数调用的行。函数调用本身将求值为函数的返回值。
请跳到第 38 行,chooseCave()
函数被调用的地方:
caveNumber = chooseCave()
在第 38 行,当程序调用chooseCave()
函数时,该函数在第 11 行定义,函数调用会评估cave
中的字符串,然后存储在caveNumber
中。chooseCave()
内的while
循环确保chooseCave()
只会返回'1'
或'2'
作为其返回值。因此caveNumber
只能有这两个值中的一个。
有一些特殊的变量是在函数内创建的,比如第 12 行的chooseCave()
函数中的cave
变量:
cave = ''
每当调用函数时,都会创建一个局部作用域。在该函数中分配的任何变量都存在于局部作用域中。将作用域视为变量的容器。局部作用域中的变量之所以特殊,是因为它们在函数返回时被遗忘,并且如果再次调用函数,它们将被重新创建。局部变量的值在函数调用之间不会被记住。
在函数外分配的变量存在于全局作用域中。全局作用域只有一个,在程序开始时创建。当程序终止时,全局作用域被销毁,其中的所有变量都被遗忘。否则,下次运行程序时,变量将记住上次运行时的值。
本地范围存在的变量称为本地变量,而全局范围存在的变量称为全局变量。变量必须是其中之一;它不能既是本地的又是全局的。
变量cave
是在chooseCave()
函数内创建的。这意味着它是在chooseCave()
函数的本地范围内创建的。当chooseCave()
返回时,它将被遗忘,并且如果第二次调用chooseCave()
,它将被重新创建。
本地和全局变量可以具有相同的名称,但它们是不同的变量,因为它们在不同的范围内。让我们编写一个新程序来说明这些概念:
def bacon():
? spam = 99 # Creates a local variable named spam
? print(spam) # Prints 99
? spam = 42 # Creates a global variable named spam
? print(spam) # Prints 42
? bacon() # Calls the bacon() function and prints 99
? print(spam) # Prints 42
我们首先创建一个名为bacon()
的函数。在bacon()
中,我们创建一个名为spam
的变量,并将其赋值为99
?。在 ?,我们调用print()
来打印这个本地的spam
变量,它是99
。在 ?,还声明了一个名为spam
的全局变量,并将其设置为42
。这个变量是全局的,因为它在所有函数之外。当全局spam
变量传递给print()
时,在 ?,它打印42
。当在 ?调用bacon()
函数时,?和 ?被执行,并且本地的spam
变量被创建,设置,然后打印。因此调用bacon()
会打印值99
。在bacon()
函数调用返回后,本地的spam
变量被遗忘。如果我们在 ?打印spam
,我们正在打印全局变量,因此输出为42
。
运行时,此代码将输出以下内容:
42
99
42
变量的创建位置决定了它所在的范围。在编写程序时,请记住这一点。
Dragon Realm 程序中定义的下一个函数名为checkCave()
。
def checkCave(chosenCave):
请注意括号之间的文本chosenCave
。这是一个参数:函数代码使用的本地变量。当调用函数时,调用的参数是分配给参数的值。
让我们回到交互式 shell 片刻。请记住,对于某些函数调用,如str()
或randint()
,您需要在括号之间传递一个或多个参数:
>>> str(5)
'5'
>>> random.randint(1, 20)
14
>>> len('Hello')
5
此示例包括一个您尚未见过的 Python 函数:len()
。len()
函数返回一个整数,指示传递给它的字符串中有多少个字符。在这种情况下,它告诉我们字符串'Hello'
有5
个字符。
当调用checkCave()
时,您还将传递一个参数。此参数存储在名为chosenCave
的新变量中,该变量是checkCave()
的参数。
以下是一个演示如何定义带有参数(name
)的函数(sayHello
)的简短程序:
def sayHello(name):
print('Hello, ' + name + '. Your name has ' + str(len(name)) +
' letters.')
sayHello('Alice')
sayHello('Bob')
spam = 'Carol'
sayHello(spam)
当您在括号中调用sayHello()
并传递参数时,参数被分配给name
参数,并执行函数中的代码。sayHello()
函数中只有一行代码,即print()
函数调用。在print()
函数调用中有一些字符串和name
变量,以及对len()
函数的调用。在这里,len()
用于计算name
中的字符数。如果运行程序,输出如下:
Hello, Alice. Your name has 5 letters.
Hello, Bob. Your name has 3 letters.
Hello, Carol. Your name has 5 letters.
对于每次调用sayHello()
,都会打印问候语和name
参数的长度。请注意,因为字符串'Carol'
赋给了spam
变量,sayHello(spam)
等同于sayHello('Carol')
。
让我们回到 Dragon Realm 游戏的源代码:
print('You approach the cave...')
time.sleep(2)
time
模块有一个名为sleep()
的函数,可以暂停程序。第 21 行传递整数值2
,以便time.sleep()
将程序暂停 2 秒。
print('It is dark and spooky...')
time.sleep(2)
在这里,代码打印了一些文本,并等待另外 2 秒。这些短暂的暂停增加了游戏的悬念,而不是一次性显示所有文本。在第 4 章的笑话程序中,您调用input()
函数以暂停,直到玩家按下 ENTER 键。在这里,玩家除了等待几秒钟外,不必做任何事情。
print('A large dragon jumps out in front of you! He opens his jaws
and...')
print()
time.sleep(2)
悬念逐渐增加,我们的程序接下来将确定哪个洞穴有友好的龙。
第 28 行调用randint()
函数,它将随机返回1
或2
。
friendlyCave = random.randint(1, 2)
这个整数值存储在friendlyCave
中,并指示拥有友好龙的洞穴。
if chosenCave == str(friendlyCave):
print('Gives you his treasure!')
第 30 行检查玩家在chosenCave
变量中选择的洞穴('1'
或'2'
)是否等于友好的龙洞穴。
但friendlyCave
中的值是一个整数,因为randint()
返回整数。你不能用==
符号比较字符串和整数,因为它们永远不会相等:'1'
不等于1
,'2'
不等于2
。
所以friendlyCave
传递给str()
函数,它返回friendlyCave
的字符串值。现在值将具有相同的数据类型,并且可以有意义地相互比较。我们也可以将chosenCave
转换为整数值。然后第 30 行将如下所示:
if int(chosenCave) == friendlyCave:
如果chosenCave
等于friendlyCave
,条件求值为True
,第 31 行告诉玩家他们赢得了宝藏。
现在我们必须添加一些代码来运行,如果条件为假。第 32 行是一个else
语句:
else:
print('Gobbles you down in one bite!')
else
语句只能在if
块之后出现。如果if
语句的条件为False
,则else
块执行。可以将其视为程序的一种方式,即“如果此条件为真,则执行if
块,否则执行else
块”。
在这种情况下,当chosenCave
不等于friendlyCave
时,else
语句运行。然后,运行第 33 行的print()
函数调用,告诉玩家他们被龙吃掉了。
程序的第一部分定义了几个函数,但没有运行它们内部的代码。第 35 行是程序的主要部分开始的地方,因为它是第一行运行的地方:
playAgain = 'yes'
while playAgain == 'yes' or playAgain == 'y':
这一行是程序的主要部分开始的地方。之前的def
语句只是定义了函数。它们没有运行这些函数内部的代码。
第 35 和 36 行设置了一个包含游戏其余代码的循环。在游戏结束时,玩家可以告诉程序他们是否想再玩一次。如果是,执行进入while
循环,再次运行整个游戏。如果不是,while
语句的条件将为False
,执行将移动到程序的末尾并终止。
第一次执行到这个while
语句时,第 35 行刚刚将’yes’赋给了playAgain
变量。这意味着在程序开始时条件将为True
。这保证了执行至少进入循环一次。
第 37 行调用displayIntro()
函数:
displayIntro()
这不是一个 Python 函数——它是你在第 4 行之前定义的函数。当调用此函数时,程序执行跳转到第 5 行的displayIntro()
函数的第一行。当函数中的所有行都被执行后,执行跳回到第 37 行并继续向下移动。
第 38 行还调用了一个你定义的函数:
caveNumber = chooseCave()
记住chooseCave()
函数允许玩家选择他们想要进入的洞穴。当第 17 行的return cave
执行时,程序执行跳回到第 38 行。chooseCave()
调用然后评估返回值,这将是一个整数值,代表玩家选择进入的洞穴。这个返回值存储在一个名为caveNumber
的新变量中。
然后程序执行继续到第 39 行:
checkCave(caveNumber)
第 39 行调用checkCave()
函数,将caveNumber
中的值作为参数传递。执行不仅跳转到第 20 行,而且caveNumber
中的值被复制到checkCave()
函数内的参数chosenCave
中。这是根据玩家选择进入的洞穴显示'给你他的宝藏!'
或'一口吞下你!'
的函数。
无论玩家赢还是输,都会被问是否要再玩一次。
print('Do you want to play again? (yes or no)')
playAgain = input()
变量playAgain
存储玩家输入的内容。第 42 行是while
块的最后一行,因此程序跳回到第 36 行检查while
循环的条件:playAgain == 'yes' or playAgain == 'y'
。
如果玩家输入字符串'yes'
或'y'
,则执行将再次进入 37 行的循环。
如果玩家输入'no'
或'n'
或类似'亚伯拉罕·林肯'
的愚蠢字符串,则条件为False
,程序执行将继续到while
块后的行。但由于while
块后没有任何行,程序将终止。
需要注意的一件事:字符串'YES'
不等于字符串'yes'
,因为计算机不认为大小写字母是相同的。如果玩家输入字符串'YES'
,那么while
语句的条件将求值为False
,程序仍将终止。稍后,飞行棋程序将向您展示如何避免这个问题。(见“lower()
和upper()
字符串方法”在第 101 页。)
您刚刚完成了第二个游戏!在龙之境中,您使用了许多在猜数字游戏中学到的知识,并学会了一些新技巧。如果您对这个程序中的一些概念不理解,请再次查看源代码的每一行,并尝试更改代码以查看程序的变化。
在第 6 章中,您不会创建另一个游戏。相反,您将学习如何使用 IDLE 的一个特性,称为调试器。
在龙之境游戏中,您创建了自己的函数。函数是程序中的迷你程序。函数内部的代码在调用函数时运行。通过将代码分解为函数,您可以将代码组织成更短、更易于理解的部分。
参数是在函数调用时复制到函数参数中的值。函数调用本身会求值为返回值。
您还学习了变量作用域。在函数内创建的变量存在于局部作用域中,而在所有函数之外创建的变量存在于全局作用域中。全局作用域中的代码无法使用局部变量。如果局部变量与全局变量同名,Python 会将其视为单独的变量。将新值赋给局部变量不会改变全局变量的值。
变量作用域可能看起来很复杂,但对于将函数组织为程序其余部分的独立代码块非常有用。因为每个函数都有自己的局部作用域,所以您可以确保一个函数中的代码不会导致其他函数中的错误。
几乎每个程序都有函数,因为它们非常有用。通过理解函数的工作原理,您可以节省大量的输入,并使错误更容易修复。
原文:
inventwithpython.com/invent4thed/chapter6.html
译者:飞龙
如果你输入错误的代码,计算机就不会给你正确的程序。计算机程序总是会按照你告诉它的去做,但你告诉它的可能不是你真正想让它做的。这些错误是计算机程序中的bug。当程序员没有仔细考虑程序到底在做什么时,就会出现 bug。
本章涵盖的主题
三种类型的错误
IDLE 的调试器
Go 和 Quit 按钮
步入、步过和步出
断点
你的程序可能会出现三种类型的错误:
语法错误 这种 bug 来自于拼写错误。当 Python 解释器遇到语法错误时,是因为你的代码没有按照正确的 Python 语言编写。即使是一个语法错误的 Python 程序也无法运行。
运行时错误 这些 bug 发生在程序运行时。程序会一直运行直到它到达有错误的那行代码,然后程序会以错误消息终止(这称为崩溃)。Python 解释器会显示一个回溯:显示包含问题的那行的错误消息。
语义错误 这些 bug 是最难修复的,它们不会使程序崩溃,但会阻止程序做程序员打算让它做的事情。例如,如果程序员希望变量total
是变量a
、b
和c
中值的和,但写成了total = a * b * c
,那么total
中的值就是错误的。这可能会在程序后面导致程序崩溃,但是语义 bug 发生的地方可能不会立即显而易见。
发现程序中的 bug 可能很困难,甚至你可能都没有注意到它们!运行程序时,你可能会发现有时函数没有在应该调用它们的时候被调用,或者可能调用了太多次。你可能会错误地编写while
循环的条件,导致它循环的次数不对。你可能会写一个永远不会退出的循环,这种语义错误称为无限循环。要停止陷入无限循环的程序,你可以在交互式 shell 中按下 CTRL-C。
实际上,通过在交互式 shell 中输入以下代码来创建一个无限循环(记得按两次 ENTER 来让交互式 shell 知道你已经输入完while
块):
>>> while True:
print('Press Ctrl-C to stop this infinite loop!!!')
现在按住 CTRL-C 来停止程序。交互式 shell 会显示如下内容:
Press Ctrl-C to stop this infinite loop!!!
Press Ctrl-C to stop this infinite loop!!!
Press Ctrl-C to stop this infinite loop!!!
Press Ctrl-C to stop this infinite loop!!!
Press Ctrl-C to stop this infinite loop!!!
Traceback (most recent call last):
File "<pyshell#1>", line 2, in <module>
print('Press Ctrl-C to stop this infinite loop!!!')
File "C:\Program Files\Python 3.5\lib\idlelib\PyShell.py", line 1347, in
write
return self.shell.write(s, self.tags)
KeyboardInterrupt
while
循环总是True
,所以程序会一直打印同一行,直到用户停止。在这个例子中,我们按下 CTRL-C 来停止无限循环,while
循环执行了五次后。
要找出 bug 的来源可能很困难,因为代码行执行得很快,变量中的值也经常变化。调试器是一个程序,它让你逐行步进你的代码,按照 Python 执行每条指令的顺序。调试器还会显示每一步中变量中存储的值。
在 IDLE 中,打开你在第 5 章中制作的 Dragon Realm 游戏。打开dragon.py文件后,点击交互式 shell,然后点击调试 !image](https://gitcode.net/OpenDocCN/flygon-trans-202401/-/raw/master/docs/inv-uron-cmp-gm-py-4e/img/6213f577c15feb006bdab7161d1cfc75.png) 调试器以显示调试控制窗口([图 6-1)。
当调试器运行时,调试控制窗口会显示如图 6-2。确保选择堆栈、本地、源和全局复选框。
现在,当您按下 F5 运行 Dragon Realm 游戏时,IDLE 的调试器将激活。这被称为在调试器下运行程序。当您在调试器下运行 Python 程序时,程序将在执行第一条指令之前停止。如果您点击文件编辑器的标题栏(并且在调试控制窗口中选择了源代码复选框),第一条指令将会被用灰色突出显示。调试控制窗口显示执行位于第 1 行,即import random
行。
图 6-1:调试控制窗口
图 6-2:在调试器下运行 Dragon Realm 游戏
调试器允许您一次执行一条指令;这个过程称为步进。现在通过点击调试控制窗口中的步进按钮来执行单个指令。Python 将执行import random
指令,然后在执行下一条指令之前停止。调试控制窗口会显示当您点击“步进”按钮时即将执行的行,所以现在执行应该在第 2 行,即import time
行。点击退出按钮暂时终止程序。
以下是在调试器下运行 Dragon Realm 时点击“步进”按钮时发生的情况的摘要。按 F5 重新开始运行 Dragon Realm,然后按照以下说明操作:
点击“步进”按钮两次来运行两个import
行。
点击步进按钮三次来执行三个def
语句。
再次点击步进按钮来定义playAgain
变量。
点击运行来运行程序的其余部分,或点击退出来终止程序。
调试器跳过了第 3 行,因为它是空行。请注意,您只能使用调试器向前步进;无法后退。
调试控制窗口中的全局区域是显示所有全局变量的地方。请记住,全局变量是在任何函数之外创建的变量(即在全局范围内)。
在全局区域中函数名称旁边的文本看起来像"<function checkCave at 0x012859B0>"
。模块名称旁边也有令人困惑的文本,如"<module 'random' from 'C:\\Python31\\lib\\random.pyc'>"
。您不需要知道这些文本的含义来调试程序。只需查看函数和模块是否在全局区域中,就可以知道函数是否已定义或模块是否已导入。
您还可以忽略全局区域中的__builtins__
、__doc__
、__name__
和其他类似行。 (这些是出现在每个 Python 程序中的变量。)
在 Dragon Realm 程序中,三个def
语句,用于执行和定义函数,将出现在调试控制窗口的全局区域中。当创建playAgain
变量时,它也将显示在全局区域中。变量名称旁边将是字符串’yes’。调试器允许您在程序运行时查看所有变量的值。这对于修复错误很有用。
除了全局区域,还有一个局部区域,它显示了局部范围变量及其值。当程序执行位于函数内部时,局部区域将包含变量。当执行位于全局范围时,此区域为空。
如果您厌倦了反复点击“步进”按钮,只想让程序正常运行,点击调试控制窗口顶部的“运行”按钮。这将告诉程序正常运行而不是逐步执行。
要完全终止程序,请点击调试控制窗口顶部的退出按钮。程序将立即退出。如果您必须从程序的开始处重新开始调试,这将非常有帮助。
使用调试器启动 Dragon Realm 程序。保持步进直到调试器位于第 37 行。如图 6-3 所示,这是带有displayIntro()
的行。当您再次点击 Step 时,调试器将跳转到此函数调用并出现在第 5 行,即displayIntro()
函数中的第一行。这种步进,就是您迄今为止一直在做的,称为步进进入。
当执行暂停在第 5 行时,您将希望停止步进。如果再次单击 Step,调试器将进入print()
函数。print()
函数是 Python 的内置函数之一,因此使用调试器逐步执行它并不有用。Python 自己的函数,如print()
、input()
、str()
和randint()
,已经经过仔细检查,不会导致程序中的错误。您可以假设它们不是导致程序错误的部分。
您不希望浪费时间逐步执行print()
函数的内部。因此,不要点击 Step 以进入print()
函数的代码,而是点击Over。这将跳过print()
函数内部的代码。print()
内部的代码将以正常速度执行,然后一旦执行返回print()
,调试器将暂停。
跳过是一种方便的方式,可以跳过函数内部的代码。现在调试器将暂停在第 38 行,caveNumber = chooseCave()
。
再次单击Step以进入chooseCave()
函数。继续逐步执行代码,直到第 15 行,即input()
调用。程序将等待您在交互式 shell 中键入响应,就像在正常运行程序时一样。如果您现在尝试单击 Step 按钮,将不会发生任何事情,因为程序正在等待键盘响应。
图 6-3:保持步进直到达到第 37 行。
返回交互式 shell 并键入您想要进入的洞穴。在您键入之前,闪烁的光标必须位于交互式 shell 的底行。否则,您键入的文本将不会出现。
一旦按下 ENTER,调试器将继续逐行执行代码。
接下来,在调试控制窗口上点击Out按钮。这被称为跳出,因为它会导致调试器跳过尽可能多的行,直到执行返回所在的函数。跳出后,执行将位于调用该函数的下一行。
如果您不在一个函数内部,单击 Out 将导致调试器执行程序中的所有剩余行。这与单击 Go 按钮时发生的行为相同。
以下是每个按钮的功能概述:
Go正常执行剩余的代码,或者直到达到断点(参见“设置断点”第 73 页)。
Step执行一条指令或一步。如果该行是一个函数调用,调试器将进入该函数。
Over执行一条指令或一步。如果该行是一个函数调用,调试器不会步进进入函数,而是跳过该调用。
Out保持跳过代码行,直到调试器离开单击 Out 时所在的函数。这会跳出该函数。
立即终止程序。
现在我们知道如何使用调试器,让我们尝试在一些程序中查找错误。
调试器可以帮助您找出程序中的错误原因。例如,这是一个带有错误的小程序。该程序为用户提供一个随机的加法问题。在交互式 shell 中,单击文件新建窗口以打开一个新的文件编辑器窗口。将此程序输入到该窗口中,并将其保存为buggy.py。
buggy.py
import random
number1 = random.randint(1, 10)
number2 = random.randint(1, 10)
print('What is ' + str(number1) + ' + ' + str(number2) + '?')
answer = input()
if answer == number1 + number2:
print('Correct!')
else:
print('Nope! The answer is ' + str(number1 + number2))
按照显示的程序输入程序,即使你已经知道错误在哪里。然后按 F5 运行程序。运行程序时可能会看起来像这样:
What is 5 + 1?
6
Nope! The answer is 6
这是一个错误!程序不会崩溃,但它没有正确工作。即使输入了正确答案,程序也会说用户是错误的。
在调试器下运行程序将有助于找到错误的原因。在交互式 shell 的顶部,点击Debug Debugger来显示调试控制窗口。(确保你已经勾选了Stack、Source、Locals和Globals复选框。)然后在文件编辑器中按 F5 运行程序。这次它将在调试器下运行。
调试器从import random
行开始。
import random
这里没有发生特别的事情,所以只需点击Step来执行它。你会看到random
模块添加到了全局区域。
再次点击Step运行第 2 行:
number1 = random.randint(1, 10)
一个新的文件编辑器窗口将出现,其中包含random.py文件。你已经进入了random
模块内部的randint()
函数。你知道 Python 的内置函数不会是你的错误的源头,所以点击Out来从randint()
函数中跳出,回到你的程序。然后关闭random.py文件的窗口。下次,你可以点击 Over 来跳过randint()
函数的步骤,而不是进入它。
第 3 行也是一个randint()
函数调用:
number2 = random.randint(1, 10)
通过点击Over跳过进入这段代码的步骤。
第 4 行是一个print()
调用,用来显示随机数给玩家看:
print('What is ' + str(number1) + ' + ' + str(number2) + '?')
你甚至在打印它们之前就知道程序将打印什么数字!只需查看调试控制窗口的全局区域。你可以看到number1
和number2
变量,旁边是存储在这些变量中的整数值。
number1
变量的值为4
,number2
变量的值为8
。(你的随机数可能会不同。)当你点击 Step 时,str()
函数将连接这些整数的字符串版本,并且程序将用这些值在print()
调用中显示字符串。当我运行调试器时,它看起来像图 6-4。
点击Step从第 5 行执行input()
。
answer = input()
调试器会等待玩家在程序中输入响应。在交互式 shell 中输入正确的答案(在我的情况下是 12)。调试器将恢复并移动到第 6 行:
if answer == number1 + number2:
print('Correct!')
第 6 行是一个if
语句。条件是answer
的值必须与number1
和number2
的和相匹配。如果条件为True
,调试器将移动到第 7 行。如果条件为False
,调试器将移动到第 9 行。点击Step再次找出它去哪里了。
else:
print('Nope! The answer is ' + str(number1 + number2))
图 6-4:number1 设置为 4,而 number2 设置为 8。
现在调试器在第 9 行!发生了什么?if
语句中的条件必须是False
。查看number1
、number2
和answer
的值。注意number1
和number2
是整数,所以它们的和也将是一个整数。但answer
是一个字符串。
这意味着answer == number1 + number2
会被计算为'12' == 12
。一个字符串值和一个整数值永远不会相等,所以条件计算为False
。
这就是程序中的错误:代码在应该使用int(answer)
时使用了answer
。将第 6 行改为int(answer) == number1 + number2
,然后再次运行程序。
What is 2 + 3?
5
Correct!
现在程序可以正确运行。再次运行程序,故意输入一个错误的答案。你现在已经调试了这个程序!记住,计算机会精确地按照你输入的程序来运行,即使你输入的不是你想要的。
逐行浏览代码可能仍然太慢。通常你会希望程序以正常速度运行,直到达到某一行。当你希望调试器在执行到达某一行时接管控制时,你可以在该行上设置一个断点。如果你认为你的代码有问题,比如在第 17 行,只需在该行(或者在该行之前几行)设置一个断点。
当执行到那一行时,程序将会进入调试器。然后你可以逐行查看发生了什么。点击“Go”将会正常执行程序,直到达到另一个断点或程序结束。
在 Windows 上设置断点,右键单击文件编辑器中的行,然后从出现的菜单中选择Set Breakpoint。在 OS X 上,CTRL-单击以打开菜单,然后选择Set Breakpoint。你可以在任意行上设置断点。文件编辑器会用黄色高亮显示每个断点行。图 6-5 显示了一个断点的示例。
图 6-5:带有两个设置断点的文件编辑器
要删除断点,点击该行并从出现的菜单中选择Clear Breakpoint。
接下来我们将看一个调用random.randint(0, 1)
来模拟抛硬币的程序。如果函数返回整数1
,那就是正面,如果返回整数0
,那就是反面。flips
变量将跟踪已经抛了多少次硬币。heads
变量将跟踪有多少次是正面。
程序将进行 1000 次抛硬币。这对一个人来说需要一个多小时,但计算机可以在一秒内完成!这个程序没有 bug,但调试器可以让我们在程序运行时查看程序的状态。将以下代码输入到文件编辑器中,并保存为coinFlips.py。如果输入此代码后出现错误,请使用在线 diff 工具将你输入的代码与本书代码进行比较,网址为www.nostarch.com/inventwithpython#diff
。
coinFlips.py
import random
print('I will flip a coin 1000 times. Guess how many times it will come up
heads. (Press enter to begin)')
input()
flips = 0
heads = 0
while flips < 1000:
if random.randint(0, 1) == 1:
heads = heads + 1
flips = flips + 1
if flips == 900:
print('900 flips and there have been ' + str(heads) + ' heads.')
if flips == 100:
print('At 100 tosses, heads has come up ' + str(heads) + ' times
so far.')
if flips == 500:
print('Halfway done, and heads has come up ' + str(heads) +
' times.')
print()
print('Out of 1000 coin tosses, heads came up ' + str(heads) + ' times!')
print('Were you close?')
程序运行得相当快。它花费更多时间等待用户按下 ENTER 键,而不是进行抛硬币。假设你想看它一次抛一个硬币。在交互式 shell 窗口中,点击Debug Debugger以打开调试控制窗口。然后按 F5 运行程序。
程序将在调试器中从第 1 行开始。在调试控制窗口中按三次Step来执行前三行(即第 1、2 和 3 行)。你会注意到按钮变为禁用状态,因为调用了input()
,交互式 shell 正在等待用户输入。点击交互式 shell 并按 ENTER 键。(确保点击交互式 shell 文本下方;否则,IDLE 可能无法接收到你的按键输入。)
你可以点击“Step”几次,但你会发现要完整运行整个程序需要相当长的时间。相反,设置断点在第 12、14 和 16 行,这样当flips
等于900
、100
和500
时,调试器就会中断。文件编辑器将会用高亮显示这些行,如图 6-6 所示。
图 6-6:在coinflips.py中设置了三个断点
设置断点后,在调试控制窗口中点击Go。程序将以正常速度运行,直到达到下一个断点。当flip
设置为100
时,第 13 行的if
语句条件为True
。这导致执行第 14 行(其中设置了断点),告诉调试器停止程序并接管控制。查看调试控制窗口的全局区域,看看flips
和heads
的值是多少。
再次点击Go,程序将继续执行,直到达到第 16 行的下一个断点。再次查看flips
和heads
中的值已经发生了变化。
再次点击Go以继续执行,直到达到下一个断点,即第 12 行。
编写程序只是编程的第一部分。下一部分是确保您编写的代码实际上能够正常工作。调试器可以让您逐行执行代码。您可以检查哪些行以什么顺序执行,以及变量包含什么值。当逐行执行太慢时,您可以设置断点,以便在您想要的行处停止调试器。
使用调试器是了解程序正在执行的操作的好方法。虽然本书提供了我们使用的所有游戏代码的解释,但调试器可以帮助您自己找出更多信息。
原文:
inventwithpython.com/invent4thed/chapter7.html
译者:飞龙
在本章中,你将设计一个“猜词游戏”。这个游戏比我们之前的游戏更复杂,但也更有趣。因为游戏比较复杂,我们将首先通过在本章中创建一个流程图来仔细规划它。在第 8 章中,我们将实际编写“猜词游戏”的代码。
本章涵盖的主题
ASCII 艺术
使用流程图设计程序
“猜词游戏”是一个供两个人玩的游戏,其中一名玩家想一个单词,然后在页面上为单词中的每个字母画一条空白线。然后第二名玩家尝试猜测可能在单词中的字母。
如果第二名玩家猜测字母正确,第一名玩家会在正确的空白中写下这个字母。但如果第二名玩家猜错,第一名玩家会画出一个悬挂人的一个身体部位。第二名玩家必须在悬挂人完全被画出之前猜出单词中的所有字母才能赢得游戏。
这是玩家在运行你将在第 8 章中编写的“猜词游戏”程序时可能看到的示例。玩家输入的文本是粗体。
H A N G M A N
+---+
|
|
|
===
Missed letters:
_ _ _
Guess a letter.
a
+---+
|
|
|
===
Missed letters:
_ a _
Guess a letter.
o
+---+
O |
|
|
===
Missed letters: o
_ a _
Guess a letter.
r
+---+
O |
| |
|
===
Missed letters: or
_ a _
Guess a letter.
t
+---+
O |
| |
|
===
Missed letters: or
_ a t
Guess a letter.
a
You have already guessed that letter. Choose again.
Guess a letter.
c
Yes! The secret word is "cat"! You have won!
Do you want to play again? (yes or no)
no
“猜词游戏”的图形是打印在屏幕上的键盘字符。这种图形称为ASCII 艺术(发音为ask-ee),这是表情符号的一种前身。这是一个用 ASCII 艺术绘制的猫的例子:
“猜词游戏”的图片将如下 ASCII 艺术所示:
+---+ +---+ +---+ +---+ +---+ +---+ +---+
| O | O | O | O | O | O |
| | | | /| | /|\ | /|\ | /|\ |
| | | | | / | / \ |
=== === === === === === ===
这个游戏比你到目前为止见过的游戏要复杂一些,所以让我们花点时间思考它是如何组合在一起的。首先,你将创建一个流程图(就像第 5-1 章上的那个,第 47 页上的“龙之境界”游戏)来帮助可视化这个程序将要做什么。
如第 5 章中所讨论的,流程图是一种显示一系列步骤的图表,其中的框用箭头连接。每个框代表一个步骤,箭头显示可能的下一步。把手指放在流程图的 START 框上,然后沿着箭头跟踪程序,直到到达 END 框。你只能按箭头的方向从一个框移动到另一个框。除非有一个指向后退的箭头,否则你永远不能后退,就像“玩家已经猜过这个字母”框中那样。
图 7-1 是“猜词游戏”的完整流程图。
图 7-1:猜词游戏的完整流程图
当然,你不一定要制作流程图;你可以直接开始编写代码。但通常一旦你开始编程,你会想到必须添加或更改的东西。你可能最终不得不删除大量的代码,这将是一种浪费。为了避免这种情况,最好在开始编写代码之前计划程序将如何工作。
你的流程图不一定要像图 7-1 中的那样。只要你能理解你的流程图,在你开始编码时它就会很有帮助。你可以从一个 START 和一个 END 框开始制作流程图,就像图 7-2 中所示的那样。
现在想想当你玩“猜词游戏”时会发生什么。首先,计算机会想一个秘密单词。然后玩家猜字母。为这些事件添加框,就像图 7-3 中所示的那样。每个流程图中的新框都有虚线轮廓。
图 7-2:用一个 START 和一个 END 框开始你的流程图。
图 7-3:用描述绘制“吊死人”的前两个步骤。
但是玩家猜一个字母后游戏并不会结束。程序需要检查该字母是否在秘密单词中。
有两种可能性:字母要么在单词中,要么不在。你将为流程图添加两个新框,每种情况一个。这在流程图中创建了一个分支,如图 7-4 所示。
图 7-4:分支有两个箭头指向不同的框。
如果字母在秘密单词中,检查玩家是否猜对了所有字母并赢得了比赛。如果字母不在秘密单词中,检查吊死人是否完成并且玩家已经输了。也为这些情况添加框。
流程图现在看起来像图 7-5。
图 7-5:分支后,步骤继续沿着各自的路径。
你不需要从“字母在秘密单词中”框到“玩家猜测次数用完并且失败”框的箭头,因为如果玩家刚刚猜对,他们不可能失败。如果玩家刚刚猜错,他们也不可能赢,所以你也不需要为此画箭头。
玩家赢了或输了之后,询问他们是否想要用新的秘密单词再玩一次。如果玩家不想再玩,程序结束;否则,程序继续并想出一个新的秘密单词。这在图 7-6 中显示。
图 7-6:在要求玩家再次玩之后,流程图分支出去。
流程图现在看起来大部分都完成了,但我们还缺少一些东西。首先,玩家不只猜一个字母;他们会一直猜字母直到赢或输。如图 7-7 所示,画出两个新的箭头。
如果玩家再次猜相同的字母怎么办?与其再次计数这个字母,不如允许他们猜一个不同的字母。这个新框显示在图 7-8 中。
图 7-7:虚线箭头显示玩家可以再次猜测。
图 7-8:添加一个步骤,以防玩家猜测他们已经猜过的字母。
如果玩家两次猜相同的字母,流程图会回到“要求玩家猜一个字母”的框。
玩家需要知道他们在游戏中的表现如何。程序应该向他们显示吊死人的图画和秘密单词(未猜出的字母用空格表示)。这些视觉效果将让他们看到自己离赢得或输掉比赛有多近。
每次玩家猜测一个字母时,此信息都会更新。在“想一个秘密单词”框和“要求玩家猜一个字母”框之间的流程图中添加一个“向玩家显示图画和空白”框,如图 7-9 所示。
图 7-9:添加一个“向玩家显示图画和空白”框,以向玩家提供反馈。
看起来不错!这个流程图完全映射了“吊死人”游戏中可能发生的一切顺序。当你设计自己的游戏时,流程图可以帮助你记住需要编写的所有内容。
首先草拟程序的流程图可能看起来很费力。毕竟,人们想玩游戏,而不是看流程图!但是通过在编写代码之前考虑程序的工作原理,可以更容易地进行更改和识别问题。
如果你首先开始编写代码,你可能会发现一些问题,这些问题需要你改变已经编写的代码,浪费时间和精力。每次改变代码时,你都有可能因改变过少或过多而产生新的错误。在构建之前知道你想要构建什么要高效得多。现在我们有了一个流程图,让我们在第 8 章中创建猜词游戏程序!
原文:
inventwithpython.com/invent4thed/chapter8.html
译者:飞龙
本章的游戏引入了许多新概念,但不用担心:在实际编写游戏之前,你将在交互式 shell 中进行实验。你将学习方法,这些方法是附加到值上的函数。你还将学习一个叫做list的新数据类型。一旦你理解了这些概念,编写 Hangman 游戏将会更容易。
本章涵盖的主题
列表
in
运算符
方法
split()
、lower()
、upper()
、startswith()
和endswith()
字符串方法
elif
语句
本章的游戏比之前的游戏要长一些,但其中大部分是用于绘制悬吊人图片的 ASCII 艺术。将以下内容输入到文件编辑器中,并将其保存为hangman.py。如果输入以下代码后出现错误,请使用在线 diff 工具将你输入的代码与书中的代码进行比较。
hangman.py
import random
HANGMAN_PICS = ['''
+---+
|
|
|
===''', '''
+---+
O |
|
|
===''', '''
+---+
O |
| |
|
===''', '''
+---+
O |
/| |
|
===''', '''
+---+
O |
/|\ |
|
===''', '''
+---+
O |
/|\ |
/ |
===''', '''
+---+
O |
/|\ |
/ \ |
===''']
words = 'ant baboon badger bat bear beaver camel cat clam cobra cougar
coyote crow deer dog donkey duck eagle ferret fox frog goat goose hawk
lion lizard llama mole monkey moose mouse mule newt otter owl panda
parrot pigeon python rabbit ram rat raven rhino salmon seal shark sheep
skunk sloth snake spider stork swan tiger toad trout turkey turtle
weasel whale wolf wombat zebra'.split()
def getRandomWord(wordList):
# This function returns a random string from the passed list of
strings.
wordIndex = random.randint(0, len(wordList) - 1)
return wordList[wordIndex]
def displayBoard(missedLetters, correctLetters, secretWord):
print(HANGMAN_PICS[len(missedLetters)])
print()
print('Missed letters:', end=' ')
for letter in missedLetters:
print(letter, end=' ')
print()
blanks = '_' * len(secretWord)
for i in range(len(secretWord)): # Replace blanks with correctly
guessed letters.
if secretWord[i] in correctLetters:
blanks = blanks[:i] + secretWord[i] + blanks[i+1:]
for letter in blanks: # Show the secret word with spaces in between
each letter.
print(letter, end=' ')
print()
def getGuess(alreadyGuessed):
# Returns the letter the player entered. This function makes sure the
player entered a single letter and not something else.
while True:
print('Guess a letter.')
guess = input()
guess = guess.lower()
if len(guess) != 1:
print('Please enter a single letter.')
elif guess in alreadyGuessed:
print('You have already guessed that letter. Choose again.')
elif guess not in 'abcdefghijklmnopqrstuvwxyz':
print('Please enter a LETTER.')
else:
return guess
def playAgain():
# This function returns True if the player wants to play again;
otherwise, it returns False.
print('Do you want to play again? (yes or no)')
return input().lower().startswith('y')
print('H A N G M A N')
missedLetters = ''
correctLetters = ''
secretWord = getRandomWord(words)
gameIsDone = False
while True:
displayBoard(missedLetters, correctLetters, secretWord)
# Let the player enter a letter.
guess = getGuess(missedLetters + correctLetters)
if guess in secretWord:
correctLetters = correctLetters + guess
# Check if the player has won.
foundAllLetters = True
for i in range(len(secretWord)):
if secretWord[i] not in correctLetters:
foundAllLetters = False
break
if foundAllLetters:
print('Yes! The secret word is "' + secretWord +
'"! You have won!')
gameIsDone = True
else:
missedLetters = missedLetters + guess
# Check if player has guessed too many times and lost.
if len(missedLetters) == len(HANGMAN_PICS) - 1:
displayBoard(missedLetters, correctLetters, secretWord)
print('You have run out of guesses!\nAfter ' +
str(len(missedLetters)) + ' missed guesses and ' +
str(len(correctLetters)) + ' correct guesses,
the word was "' + secretWord + '"')
gameIsDone = True
# Ask the player if they want to play again (but only if the game is
done).
if gameIsDone:
if playAgain():
missedLetters = ''
correctLetters = ''
gameIsDone = False
secretWord = getRandomWord(words)
else:
break
Hangman 程序会从单词列表中随机选择一个秘密单词供玩家猜测。random
模块将提供这种能力,因此第 1 行导入它。
import random
但是第 2 行的HANGMAN_PICS
变量看起来与我们迄今为止看到的变量有些不同。为了理解这段代码的含义,我们需要学习一些更多的概念。
第 2 到 37 行是HANGMAN_PICS
变量的一个长赋值语句。
HANGMAN_PICS = ['''
3. +---+
|
|
|
===''', '''
--snip--
===''']
HANGMAN_PICS
变量的名称全部由大写字母组成。这是常量变量的编程约定。常量是指变量的值从第一次赋值语句开始就不会改变。尽管你可以像其他变量一样改变HANGMAN_PICS
中的值,但全大写的名称会提醒你不要这样做。
与所有约定一样,你不一定必须遵循这个约定。但这样做可以让其他程序员更容易阅读你的代码。他们会知道HANGMAN_PICS
将始终具有从第 2 行到第 37 行赋值的值。
HANGMAN_PICS
包含多个多行字符串。它可以这样做是因为它是一个列表。列表具有可以包含多个其他值的列表值。将其输入到交互式 shell 中:
>>> animals = ['aardvark', 'anteater', 'antelope', 'albert']
>>> animals
['aardvark', 'anteater', 'antelope', 'albert']
animals
中的列表值包含四个值。列表值以左方括号[
开始,以右方括号]
结束。这就像字符串以引号开始和结束一样。
逗号分隔列表中的各个值。这些值也称为项。HANGMAN_PICS
中的每个项都是一个多行字符串。
列表可以让你存储多个值,而不需要为每个值使用一个变量。如果没有列表,代码会像这样:
>>> animals1 = 'aardvark'
>>> animals2 = 'anteater'
>>> animals3 = 'antelope'
>>> animals4 = 'albert'
如果有数百或数千个字符串,这段代码将很难管理。但列表可以轻松地包含任意数量的值。
你可以通过在列表变量的末尾添加方括号并在它们之间加上一个数字来访问列表中的项目。方括号之间的数字是索引。在 Python 中,列表中第一项的索引是0
。第二项的索引是1
,第三项的索引是2
,依此类推。因为索引从0
开始而不是1
,所以我们说 Python 列表是零索引的。
在我们仍然在交互式 shell 中并且正在使用animals
列表时,输入animals[0]
,animals[1]
,animals[2]
和animals[3]
来查看它们的评估情况:
>>> animals[0]
'aardvark'
>>> animals[1]
'anteater'
>>> animals[2]
'antelope'
>>> animals[3]
'albert'
请注意,列表中的第一个值'aardvark'
存储在索引0
而不是索引1
中。列表中的每个项目都按顺序从0
开始编号。
使用方括号,您可以像对待任何其他值一样对待列表中的项目。例如,在交互式 shell 中输入animals[0] + animals[2]
:
>>> animals[0] + animals[2]
'aardvarkantelope'
animals
列表中索引0
和2
处的变量都是字符串,因此这些值被连接。计算如下所示:
如果尝试访问列表中不存在的索引,将会收到一个IndexError
,导致程序崩溃。要查看此错误的示例,请在交互式 shell 中输入以下内容:
>>> animals = ['aardvark', 'anteater', 'antelope', 'albert']
>>> animals[9999]
Traceback (most recent call last):
File "", line 1, in
animals[9999]
IndexError: list index out of range
因为索引9999
处没有值,所以会收到一个错误。
您还可以使用索引赋值更改列表中项目的值。在交互式 shell 中输入以下内容:
>>> animals = ['aardvark', 'anteater', 'antelope', 'albert']
>>> animals[1] = 'ANTEATER'
>>> animals
['aardvark', 'ANTEATER', 'antelope', 'albert']
新的'ANTEATER'
字符串覆盖了animals
列表中的第二个项目。因此,单独输入animals[1]
会计算列表当前的第二个项目,但在赋值运算符的左侧使用它会将一个新值赋给列表的第二个项目。
您可以使用+
运算符将多个列表连接成一个列表,就像对字符串一样。这样做被称为列表连接。要查看此操作,请在交互式 shell 中输入以下内容:
>>> [1, 2, 3, 4] + ['apples', 'oranges'] + ['Alice', 'Bob']
[1, 2, 3, 4, 'apples', 'oranges', 'Alice', 'Bob']
['apples'] + ['oranges']
将计算为['apples', 'oranges']
。但['apples'] + 'oranges'
将导致错误。您不能使用+
运算符将列表值和字符串值相加。如果要在列表末尾添加值而不使用列表连接,请使用append()
方法(在“reverse()
和append()
列表方法”中描述)。
in
运算符可以告诉您值是否在列表中。使用in
运算符的表达式返回一个布尔值:如果值在列表中则返回True
,否则返回`False。在交互式 shell 中输入以下内容:
>>> animals = ['aardvark', 'anteater', 'antelope', 'albert']
>>> 'antelope' in animals
True
>>> 'ant' in animals
False
表达式'antelope' in animals
返回True
,因为字符串'antelope'
是animals
列表中的值之一。它位于索引2
。但当您输入表达式'ant' in animals
时,它返回False
,因为字符串'ant'
在列表中不存在。
in
运算符也适用于字符串,检查一个字符串是否存在于另一个字符串中。在交互式 shell 中输入以下内容:
>>> 'hello' in 'Alice said hello to Bob.'
True
将多行字符串列表存储在HANGMAN_PICS
变量中涵盖了许多概念。例如,您看到列表对于在单个变量中存储多个值非常有用。您还学习了一些处理列表的技巧,比如索引赋值和列表连接。方法是您将学习如何在 Hangman 游戏中使用的另一个新概念;我们将在下面进行探讨。
方法是附加到值的函数。要调用方法,必须使用句点将其附加到特定值上。Python 有许多有用的方法,我们将在 Hangman 程序中使用其中一些。
但首先,让我们看一些列表和字符串方法。
列表数据类型有一些您可能经常使用的方法:reverse()
和append()
。reverse()
方法将颠倒列表中项目的顺序。尝试输入spam = [1, 2, 3, 4, 5, 6, 'meow', 'woof']
,然后输入spam.reverse()
以颠倒列表。然后输入spam
以查看变量的内容。
>>> spam = [1, 2, 3, 4, 5, 6, 'meow', 'woof']
>>> spam.reverse()
>>> spam
['woof', 'meow', 6, 5, 4, 3, 2, 1]
您将使用最常见的列表方法之一是append()
。此方法将传递的值添加到列表的末尾。尝试在交互式 shell 中输入以下内容:
>>> eggs = []
>>> eggs.append('hovercraft')
>>> eggs
['hovercraft']
>>> eggs.append('eels')
>>> eggs
['hovercraft', 'eels']
这些方法确实会更改它们调用的列表。它们不会返回一个新的列表。我们说这些方法会原地更改列表。
字符串数据类型有一个split()
方法,它返回一个由已分割字符串制成的字符串列表。尝试使用split()
方法,输入以下内容到交互式 shell 中:
>>> sentence = input()
My very energetic mother just served us nachos.
>>> sentence.split()
['My', 'very', 'energetic', 'mother', 'just', 'served', 'us', 'nachos.']
结果是一个包含八个字符串的列表,原始字符串中的每个单词都有一个字符串。分割发生在字符串中的空格处。这些空格不包括在列表的任何项目中。
“绞刑”程序的第 38 行也使用了split()
方法,如下所示。代码很长,但实际上只是一个简单的赋值语句,最后有一个split()
方法调用,其中有一个由空格分隔的单词的长字符串。split()
方法求值为一个列表,其中字符串中的每个单词都是一个单独的列表项。
words = 'ant baboon badger bat bear beaver camel cat clam cobra cougar
coyote crow deer dog donkey duck eagle ferret fox frog goat goose hawk
lion lizard llama mole monkey moose mouse mule newt otter owl panda
parrot pigeon python rabbit ram rat raven rhino salmon seal shark sheep
skunk sloth snake spider stork swan tiger toad trout turkey turtle
weasel whale wolf wombat zebra'.split()
使用split()
来编写这个程序会更容易。如果一开始就创建了一个列表,你将不得不为每个单词输入['ant', 'baboon', 'badger'
等等,每个单词都要加上引号和逗号。
你也可以在第 38 行的字符串中添加自己的单词,或者删除你不想在游戏中出现的任何单词。只要确保单词之间有空格。
第 40 行定义了getRandomWord()
函数。一个列表参数将被传递给它的wordList
参数。这个函数将从wordList
中返回一个秘密单词。
def getRandomWord(wordList):
# This function returns a random string from the passed list of
strings.
wordIndex = random.randint(0, len(wordList) - 1)
return wordList[wordIndex]
在第 42 行,我们通过调用randint()
并传入两个参数,将这个列表的随机索引存储在wordIndex
变量中。第一个参数是0
(表示第一个可能的索引),第二个参数是表达式len(wordList) - 1
的值(表示wordList
中的最后一个可能的索引)。
请记住,列表索引从0
开始,而不是1
。如果你有一个包含三个项目的列表,第一个项目的索引是0
,第二个项目的索引是1
,第三个项目的索引是2
。这个列表的长度是 3,但索引3
将在最后一个索引之后。这就是为什么第 42 行从wordList
的长度中减去1
。第 42 行的代码将在wordList
的大小如何都能工作。现在,如果你愿意,你可以添加或删除wordList
中的字符串。
wordIndex
变量将被设置为传递给wordList
参数的列表的随机索引。第 43 行将返回存储在wordIndex
中的整数的wordList
中的元素。
假设['apple', 'orange', grape']
被传递为getRandomWord()
的参数,并且randint(0, 2)
返回整数2
。这意味着第 43 行将求值为return wordList[2]
,然后求值为return 'grape'
。这就是getRandomWord()
如何从wordList
中返回一个随机字符串。
因此,getRandomWord()
的输入是一个字符串列表,返回值输出是从该列表中随机选择的字符串。在“绞刑”游戏中,这就是玩家要猜测的秘密单词是如何选择的。
接下来,您需要一个函数来在屏幕上打印“绞刑”板。它还应该显示玩家已经正确(和错误)猜到了多少个字母。
def displayBoard(missedLetters, correctLetters, secretWord):
print(HANGMAN_PICS[len(missedLetters)])
print()
这段代码定义了一个名为displayBoard()
的新函数。这个函数有三个参数:
missedLetters
玩家猜测的不在秘密单词中的字母的字符串
correctLetters
玩家猜测的在秘密单词中的字母的字符串
secretWord
玩家试图猜测的秘密单词的字符串
第一个print()
函数调用将显示板。全局变量HANGMAN_PICS
有一个字符串列表,每个可能的板都有一个字符串。(请记住,全局变量可以从函数内部读取。)HANGMAN_PICS[0]
显示一个空的绞刑架,HANGMAN_PICS[1]
显示头部(当玩家错过一个字母时),HANGMAN_PICS[2]
显示头部和身体(当玩家错过两个字母时),依此类推,直到HANGMAN_PICS[6]
,显示完整的绞刑架。
missedLetters
中的字母数量将反映玩家猜错的次数。调用len(missedLetters)
来找出这个数字。因此,如果missedLetters
是'aetr'
,那么len('aetr'
)将返回4
。打印HANGMAN_PICS[4]
将显示四次猜错时适当的吊死人图片。这就是第 46 行中HANGMAN_PICS[len(missedLetters)]
的评估结果。
第 49 行打印了字符串'Missed letters:'
,末尾带有空格字符而不是换行符:
print('Missed letters:', end=' ')
for letter in missedLetters:
print(letter, end=' ')
print()
第 50 行的for
循环将遍历字符串missedLetters
中的每个字符并将其打印到屏幕上。请记住,end=' '
将用单个空格字符替换在字符串后打印的换行符。例如,如果missedLetters
是'ajtw'
,这个for
循环将显示a j t w
。
displayBoard()
函数的其余部分(第 54 至 62 行)显示了错过的字母,并创建了一个包含所有尚未猜出字母的秘密单词的字符串。它使用range()
函数和列表切片来实现这一点。
当使用一个参数调用range()
时,它将返回一个整数范围对象,从0
到(但不包括)参数。这个范围对象用于for
循环,但也可以用list()
函数转换为更熟悉的列表数据类型。将list(range(10))
输入到交互式 shell 中:
>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list('Hello')
['H', 'e', 'l', 'l', 'o']
list()
函数类似于str()
或int()
函数。它接受传递给它的值并返回一个列表。使用range()
函数很容易生成大型列表。例如,输入list(range(10000))
到交互式 shell 中:
>>> list(range(10000))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,...
--snip--
...9989, 9990, 9991, 9992, 9993, 9994, 9995, 9996, 9997, 9998, 9999]
列表太大,甚至无法放入屏幕。但是您可以将列表存储在一个变量中:
>>> spam = list(range(10000))
如果向range()
传递两个整数参数,则它返回的范围对象是从第一个整数参数到第二个整数参数(但不包括第二个整数参数)。接下来,将list(range(10, 20))
输入到交互式 shell 中:
>>> list(range(10, 20))
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
如您所见,我们的列表只到 19,不包括 20。
列表切片使用另一个列表的子集创建一个新的列表值。要切片一个列表,请在列表名称后的方括号中指定两个索引(开始和结束)并用冒号分隔。例如,将以下内容输入到交互式 shell 中:
>>> spam = ['apples', 'bananas', 'carrots', 'dates']
>>> spam[1:3]
['bananas', 'carrots']
表达式spam[1:3]
求值为一个包含spam
中从索引1
到(但不包括)索引3
的项目的列表。
如果省略第一个索引,Python 将自动认为您想要第一个索引为0
:
>>> spam = ['apples', 'bananas', 'carrots', 'dates']
>>> spam[:2]
['apples', 'bananas']
如果省略第二个索引,Python 将自动认为您想要列表的其余部分:
>>> spam = ['apples', 'bananas', 'carrots', 'dates']
>>> spam[2:]
['carrots', 'dates']
您还可以像对列表一样对字符串使用切片。字符串中的每个字符就像列表中的一个项目。将以下内容输入到交互式 shell 中:
>>> myName = 'Zophie the Fat Cat'
>>> myName[4:12]
'ie the F'
>>> myName[:10]
'Zophie the'
>>> myName[7:]
'the Fat Cat'
Hangman 代码的下一部分使用了切片。
现在,您想打印出秘密单词,但对于尚未猜出的字母,您可以使用下划线字符(_
)来表示。首先,为秘密单词中的每个字母创建一个仅包含一个下划线的字符串。然后,用correctLetters
中的每个字母替换空白。
因此,如果秘密单词是'otter'
,那么被掩盖的字符串将是'_____'
(五个下划线)。如果correctLetters
是字符串'rt'
,您将把字符串更改为'_tt_r'
。第 54 至 58 行是执行此操作的代码的一部分。
blanks = '_' * len(secretWord)
for i in range(len(secretWord)): # Replace blanks with correctly
guessed letters.
if secretWord[i] in correctLetters:
blanks = blanks[:i] + secretWord[i] + blanks[i+1:]
第 54 行使用字符串复制创建了一个由下划线组成的blanks
变量。请记住,*
运算符可以用于字符串和整数,因此表达式'_' * 5
求值为'_____'
。这将确保blanks
中的下划线数量与secretWord
中的字母数量相同。
第 56 行有一个for
循环,它遍历secretWord
中的每个字母,并用实际字母替换下划线,如果它存在于correctLetters
中。
让我们再看一下前面的示例,其中secretWord
的值为'otter'
,correctLetters
中的值为'tr'
。您希望向玩家显示字符串'_tt_r'
。让我们找出如何创建这个字符串。
第 56 行的len(secretWord)
调用会返回5
。range(len(secretWord))
调用变成了range(5)
,这使得for
循环迭代0
、1
、2
、3
和4
。
因为i
的值将取得[0, 1, 2, 3, 4]
中的每个值,所以for
循环中的代码如下:
if secretWord[0] in correctLetters:
blanks = blanks[:0] + secretWord[0] + blanks[1:]
if secretWord[1] in correctLetters:
blanks = blanks[:1] + secretWord[1] + blanks[2:]
--snip--
我们只显示了for
循环的前两次迭代,但是从0
开始,i
将取得范围内的每个数字的值。在第一次迭代中,i
取值为0
,因此if
语句检查secretWord
中索引为0
的字母是否在correctLetters
中。循环会逐个字母在secretWord
中执行此操作。
如果您对secretWord0]
或blanks[3:]
之类的值感到困惑,请查看[图 8-1。它显示了secretWord
和blanks
变量的值以及字符串中每个字母的索引。
图 8-1:blanks和secretWord字符串的索引
如果您用它们表示的值替换列表切片和列表索引,则循环代码如下:
if 'o' in 'tr': # False
blanks = '' + 'o' + '____' # This line is skipped.
--snip--
if 'r' in 'tr': # True
blanks = '_tt_' + 'r' + '' # This line is executed.
# blanks now has the value '_tt_r'.
在secretWord
为'otter'
且correctLetters
为'tr'
时,前面的代码示例都执行相同的操作。接下来的几行代码会打印blanks
的新值,每个字母之间有空格:
for letter in blanks: # Show the secret word with spaces in between
each letter.
print(letter, end=' ')
print()
请注意,第 60 行的for
循环没有调用range()
函数。它不是在范围对象上进行迭代,而是在blanks
变量中的字符串值上进行迭代。在每次迭代中,letter
变量从blanks
中的'otter'
字符串中取出一个新字符。
在添加空格后打印输出将是'_ t t _ r'
。
将调用getGuess()
函数,以便玩家可以输入一个猜测的字母。该函数将字母作为字符串返回。此外,getGuess()
将确保玩家在返回函数之前输入有效的字母。
def getGuess(alreadyGuessed):
# Returns the letter the player entered. This function makes sure the
player entered a single letter and not something else.
玩家猜测的字母串作为alreadyGuessed
参数传递。然后getGuess()
函数要求玩家猜测一个字母。这个单个字母将是getGuess()
的返回值。现在,因为 Python 是区分大小写的,我们需要确保玩家的猜测是小写字母,以便我们可以将其与秘密单词进行比较。这就是lower()
方法的用武之地。
在交互式 shell 中输入'Hello world!'.lower()
来查看lower()
方法的示例:
>>> 'Hello world!'.lower()
'hello world!'
lower()
方法返回一个所有字符都为小写的字符串。字符串还有一个upper()
方法,它返回一个所有字符都为大写的字符串。尝试输入'Hello world!'.upper()
到交互式 shell 中:
>>> 'Hello world!'.upper()
'HELLO WORLD!'
因为upper()
方法返回一个字符串,您也可以在该字符串上调用一个方法。
现在将其输入交互式 shell:
>>> 'Hello world!'.upper().lower()
'hello world!'
'Hello world!'.upper()
求值为字符串'HELLO WORLD!'
,然后调用字符串的lower()
方法。这将返回字符串'hello world!'
,这是评估的最终值:
顺序很重要。'Hello world!'.lower().upper()
与'Hello world!'.upper().lower()
不同:
>>> 'Hello world!'.lower().upper()
'HELLO WORLD!'
该评估如下:
如果一个字符串存储在一个变量中,您也可以在该变量上调用一个字符串方法:
>>> spam = 'Hello world!'
>>> spam.upper()
'HELLO WORLD!'
这段代码不会改变spam
中的值。spam
变量仍将包含'Hello world!'
。
回到“猜词游戏”程序,我们在询问玩家猜测时使用lower()
:
while True:
print('Guess a letter.')
guess = input()
guess = guess.lower()
即使玩家输入一个大写字母作为猜测,getGuess()
函数也会返回一个小写字母。
第 66 行的while
循环将继续要求玩家输入一个字母,直到他们输入一个之前没有猜过的单个字母。
while
循环的条件只是布尔值True
。这意味着执行离开这个循环的唯一方法是执行break
语句,离开循环,或者return
语句,不仅离开循环,而且离开整个函数。
循环内的代码要求玩家输入一个字母,存储在变量guess
中。如果玩家输入一个大写字母,它将在第 69 行被覆盖为小写字母。
Hangman 程序的下一部分使用了elif
语句。你可以将elif
或else-if
语句看作是这样说的:“如果这是真的,就这样做。或者如果接下来的条件是真的,就那样做。或者如果它们都不是真的,就做最后的事情。”看一下下面的代码:
if catName == 'Fuzzball':
print('Your cat is fuzzy.')
elif catName == 'Spots':
print('Your cat is spotted.')
else:
print('Your cat is not fuzzy or spotted.')
如果catName
变量等于字符串'Fuzzball'
,那么if
语句的条件是True
,并且if
块告诉用户他们的猫是毛茸茸的。然而,如果这个条件是False
,那么 Python 接下来尝试elif
语句的条件。如果catName
是'Spots'
,那么字符串'Your cat is spotted.'
将被打印到屏幕上。如果两者都是False
,那么代码会告诉用户他们的猫既不是毛茸茸的也不是有斑点的。
你可以有任意多的elif
语句:
if catName == 'Fuzzball':
print('Your cat is fuzzy.')
elif catName == 'Spots':
print('Your cat is spotted.')
elif catName == 'Chubs':
print('Your cat is chubby.')
elif catName == 'Puff':
print('Your cat is puffy.')
else:
print('Your cat is neither fuzzy nor spotted nor chubby nor puffy.')
当elif
条件中的一个为True
时,它的代码被执行,然后执行跳到else
块之后的第一行。因此,在if
-elif
-else
语句中的块中只有一个会被执行。如果不需要else
块,也可以省略else
块,只使用if
-elif
语句。
guess
变量包含玩家的字母猜测。程序需要确保他们输入了一个有效的猜测:一个,且仅有一个,尚未被猜过的字母。如果他们没有,执行将循环回去再次要求他们输入一个字母。
if len(guess) != 1:
print('Please enter a single letter.')
elif guess in alreadyGuessed:
print('You have already guessed that letter. Choose again.')
elif guess not in 'abcdefghijklmnopqrstuvwxyz':
print('Please enter a LETTER.')
else:
return guess
第 70 行的条件检查guess
是否不是一个字符长,第 72 行的条件检查guess
是否已经存在于alreadyGuessed
变量中,第 74 行的条件检查guess
是否不是标准英语字母表中的字母。如果这些条件中有任何一个为True
,游戏会提示玩家输入一个新的猜测。
如果所有这些条件都是False
,那么else
语句的块将被执行,getGuess()
返回第 77 行的guess
的值。
记住,在if
-elif
-else
语句中,只有一个块会被执行。
playAgain()
函数只有一个print()
函数调用和一个return
语句:
def playAgain():
# This function returns True if the player wants to play again;
otherwise, it returns False.
print('Do you want to play again? (yes or no)')
return input().lower().startswith('y')
return
语句有一个看起来复杂的表达式,但你可以分解它。这是 Python 如何评估这个表达式的步骤,如果用户输入YES
:
playAgain()
函数的目的是让玩家输入 yes 或 no,告诉程序他们是否想再玩一轮 Hangman。玩家应该能够输入YES
、yes
、Y
或以y
开头的任何其他内容来表示“是”。如果玩家输入YES
,那么input()
的返回值是字符串'YES'
。'YES'.lower()
返回附加字符串的小写版本。因此'YES'.lower()
的返回值是'yes'
。
但是有第二个方法调用,startswith('y')
。这个函数如果相关的字符串以括号中的字符串参数开头,则返回True
,如果不是,则返回False
。'yes'.startswith('y')
的返回值是True
。
就是这样——你评估了这个表达式!它让玩家输入一个响应,将响应设置为小写,检查它是否以字母 y 开头,如果是,则返回 True,如果不是,则返回 False。
顺便说一句,还有一个 endswith(someString)字符串方法,如果字符串以 someString 结尾,则返回 True,如果不是,则返回 False。endswith()有点像 startswith()的相反。
这就是我们为这个游戏创建的所有函数!让我们来回顾一下它们:
getRandomWord(wordList)接受传递给它的字符串列表,并从中返回一个字符串。这就是为玩家猜测选择一个单词的方式。
显示板(missedLetters,correctLetters,secretWord)显示板的当前状态,包括玩家到目前为止猜对了多少秘密单词和玩家猜错的字母。此函数需要传递三个参数才能正确工作。correctLetters 和 missedLetters 是由玩家猜测的字母组成的字符串,分别是秘密单词中的字母和不在秘密单词中的字母。而 secretWord 是玩家试图猜测的秘密单词。此函数没有返回值。
getGuess(alreadyGuessed)接受玩家已经猜过的字母的字符串,并将继续要求玩家输入不在 alreadyGuessed 中的字母。此函数返回玩家猜测的有效字母的字符串。
playAgain()询问玩家是否想再玩一局绞刑游戏。如果玩家想玩,此函数返回 True,否则返回 False。
在函数之后,程序的主要部分从第 85 行开始。到目前为止,一切都只是函数定义和对 HANGMAN_PICS 的大型赋值语句。
绞刑游戏的主要部分显示游戏的名称,设置一些变量,并执行一个 while 循环。本节逐步介绍了程序的其余部分。
print('H A N G M A N')
missedLetters = ''
correctLetters = ''
secretWord = getRandomWord(words)
gameIsDone = False
第 85 行是游戏运行时执行的第一个 print()调用。它显示游戏的标题。接下来,将空字符串分配给 missedLetters 和 correctLetters 变量,因为玩家还没有猜测任何错误或正确的字母。
第 88 行的 getRandomWord(words)调用将求值为从 words 列表中随机选择的单词。
第 89 行将 gameIsDone 设置为 False。当代码想要发出游戏结束的信号并询问玩家是否想再玩时,代码将 gameIsDone 设置为 True。
程序的其余部分由一个 while 循环组成。循环的条件始终为 True,这意味着它将一直循环,直到遇到 break 语句为止。(这发生在第 126 行。)
while True:
displayBoard(missedLetters, correctLetters, secretWord)
第 92 行调用 displayBoard()函数,将设置在第 86、87 和 88 行的三个变量传递给它。根据玩家正确猜测和猜错的字母数量,此函数向玩家显示适当的绞刑板。
接下来调用 getGuess()函数,让玩家输入他们的猜测。
# Let the player enter a letter.
guess = getGuess(missedLetters + correctLetters)
getGuess()函数需要一个 alreadyGuessed 参数,以便它可以检查玩家是否输入了他们已经猜过的字母。第 95 行将 missedLetters 和 correctLetters 变量中的字符串连接起来,并将结果作为 alreadyGuessed 参数的参数。
如果 guess 字符串存在于 secretWord 中,则此代码将 guess 连接到 correctLetters 字符串的末尾:
if guess in secretWord:
correctLetters = correctLetters + guess
这个字符串将是 correctLetters 的新值。
程序如何知道玩家是否猜出了秘密单词中的每个字母?嗯,correctLetters
包含了玩家正确猜出的每个字母,secretWord
是秘密单词本身。但你不能简单地检查correctLetters == secretWord
。如果secretWord
是字符串otter
,而correctLetters
是字符串orte
,那么correctLetters == secretWord
将是False
,即使玩家已经猜出了秘密单词中的每个字母。
你能确定玩家赢了的唯一方法是迭代secretWord
中的每个字母,并查看它是否存在于correctLetters
中。只有当secretWord
中的每个字母都存在于correctLetters
中时,玩家才算赢了。
# Check if the player has won.
foundAllLetters = True
for i in range(len(secretWord)):
if secretWord[i] not in correctLetters:
foundAllLetters = False
break
如果在secretWord
中找到一个不在correctLetters
中的字母,你就知道玩家没有猜出所有的字母。在循环开始之前,新变量foundAllLetters
在第 101 行被设置为True
。循环开始时假设秘密单词中的所有字母都已经被找到。但是在第 104 行的循环代码中,当它发现一个不在correctLetters
中的secretWord
中的字母时,它会将foundAllLetters
改为False
。
如果秘密单词中的所有字母都被找到,玩家会被告知他们赢了,并且gameIsDone
被设置为True
:
if foundAllLetters:
print('Yes! The secret word is "' + secretWord +
'"! You have won!')
gameIsDone = True
第 109 行是else
块的开始。
else:
missedLetters = missedLetters + guess
记住,这个块中的代码将在条件为False
时执行。但是哪个条件?要找出来,把手指指向else
关键字的开始,然后向上移动。你会发现else
关键字的缩进与第 97 行的if
关键字的缩进是一样的:
if guess in secretWord:
--snip--
else:
missedLetters = missedLetters + guess
因此,如果第 97 行的条件(guess in secretWord
)是False
,那么执行将进入这个else
块。
错误猜测的字母会在第 110 行连接到missedLetters
字符串中。这就像第 98 行对玩家正确猜出的字母所做的一样。
每次玩家猜错时,代码都会将错误的字母连接到missedLetters
字符串中。因此,missedLetters
的长度——或者在代码中,len(missedLetters)
——也是错误猜测的次数。
# Check if player has guessed too many times and lost.
if len(missedLetters) == len(HANGMAN_PICS) - 1:
displayBoard(missedLetters, correctLetters, secretWord)
print('You have run out of guesses!\nAfter ' +
str(len(missedLetters)) + ' missed guesses and ' +
str(len(correctLetters)) + ' correct guesses,
the word was "' + secretWord + '"')
gameIsDone = True
HANGMAN_PICS
列表有七个 ASCII 艺术字符串。因此,当missedLetters
字符串的长度等于len(HANGMAN_PICS) - 1
(即 6)时,玩家已经用完了猜测的机会。你知道玩家已经输了,因为吊死人的图片已经完成。记住,HANGMAN_PICS[0]
是列表中的第一项,HANGMAN_PICS[6]
是最后一项。
第 115 行打印秘密单词,第 116 行将gameIsDone
变量设置为True
。
# Ask the player if they want to play again (but only if the game is
done).
if gameIsDone:
if playAgain():
missedLetters = ''
correctLetters = ''
gameIsDone = False
secretWord = getRandomWord(words)
无论玩家在猜测字母后是赢了还是输了,游戏都应该询问玩家是否想再玩一次。playAgain()
函数处理从玩家那里得到“是”或“否”,因此在第 120 行调用它。
如果玩家想再玩一次,那么missedLetters
和correctLetters
中的值必须重置为空字符串,gameIsDone
重置为False
,并且新的秘密单词存储在secretWord
中。这样,当执行回到第 91 行的while
循环的开头时,游戏板将被重置为一个新的游戏。
如果玩家在被问及是否想再玩一次时没有输入以y
开头的内容,那么第 120 行的条件将是False
,并且else
块将执行:
else:
break
break
语句导致执行跳转到循环后的第一条指令。但是因为循环后没有更多的指令,程序终止。
“Hangman”是我们迄今为止最复杂的游戏,而且在制作过程中你学到了几个新概念。随着你的游戏变得越来越复杂,草拟程序中应该发生的流程图是一个好主意。
列表是可以包含其他值的值。方法是附加到值的函数。列表有一个append()
方法。字符串有lower()
、upper()
、split()
、startswith()
和endswith()
方法。在本书的其余部分,您将学习更多的数据类型和方法。
elif
语句允许您在if
-else
语句中间添加“或者-否则如果”子句。
原文:
inventwithpython.com/invent4thed/chapter9.html
译者:飞龙
现在你已经创建了一个基本的猜词游戏,让我们看看如何通过新功能来扩展它。在本章中,你将为计算机添加多个词组,并且能够改变游戏的难度级别。
本章涵盖的主题
字典数据类型
键值对
keys()
和values()
字典方法
多变量赋值
在玩了几次猜词游戏之后,你可能会觉得六次猜测对玩家来说不够。你可以通过向HANGMAN_PICS
列表添加更多的多行字符串来轻松地给他们更多的猜测。
将你的hangman.py程序另存为hangman2.py。然后在第 37 行和之后添加以下指令,以扩展包含吊死人 ASCII 艺术的列表:
===''', '''
+---+
[O |
/|\ |
/ \ |
===''', '''
+---+
[O] |
/|\ |
/ \ |
===''']
这段代码将两个新的多行字符串添加到HANGMAN_PICS
列表中,一个是画了吊死人的左耳,另一个是画了两只耳朵。因为程序会告诉玩家他们输了,基于len(missedLetters) == len(HANGMAN_PICS) - 1
,这是你需要做的唯一改变。程序的其余部分可以很好地使用新的HANGMAN_PICS
列表。
在猜词游戏的第一个版本中,我们使用了一个动物词汇表,但是你可以在第 48 行更改单词列表。你可以有颜色:
words = 'red orange yellow green blue indigo violet white black brown'
.split()
或者形状:
words = 'square triangle rectangle circle ellipse rhombus trapezoid
chevron pentagon hexagon septagon octagon'.split()
或者水果:
words = 'apple orange lemon lime pear watermelon grape grapefruit cherry
banana cantaloupe mango strawberry tomato'.split()
通过一些修改,你甚至可以改变代码,使得猜词游戏使用动物、颜色、形状或水果等词组。程序可以告诉玩家秘密单词来自哪个词组。
要进行这种改变,你需要一个叫做字典的新数据类型。字典是一个像列表一样的值的集合。但是,你可以使用任何数据类型的索引来访问字典中的项,而不是使用整数索引。对于字典,这些索引被称为键。
字典使用{
和}
(大括号)而不是[
和]
(方括号)。在交互式 shell 中输入以下内容:
>>> spam = {'hello':'Hello there, how are you?', 4:'bacon', 'eggs':9999 }
大括号之间的值是键值对。键在冒号的左边,键的值在右边。你可以通过使用键来访问值,就像列表中的项一样。要查看一个例子,请在交互式 shell 中输入以下内容:
>>> spam = {'hello':'Hello there, how are you?', 4:'bacon', 'eggs':9999}
>>> spam['hello']
'Hello there, how are you?'
>>> spam[4]
'bacon'
>>> spam['eggs']
9999
在方括号之间不是放一个整数,而是使用一个字符串键。在spam
字典中,我使用了整数4
和字符串'eggs'
作为键。
你可以使用len()
函数获取字典中键值对的数量。例如,在交互式 shell 中输入以下内容:
>>> stuff = {'hello':'Hello there, how are you?', 4:'bacon', 'spam':9999}
>>> len(stuff)
3
len()
函数将返回一个整数值,表示键值对的数量,这里是3
。
字典和列表之间的一个区别是,字典可以有任何数据类型的键,正如你所见。但是要记住,因为0
和'0'
是不同的值,它们将成为不同的键。在交互式 shell 中输入以下内容:
>>> spam = {'0':'a string', 0:'an integer'}
>>> spam[0]
'an integer'
>>> spam['0']
'a string'
你也可以使用for
循环遍历列表和字典中的键。要查看它是如何工作的,请在交互式 shell 中输入以下内容:
>>> favorites = {'fruit':'apples', 'animal':'cats', 'number':42}
>>> for k in favorites:
print(k)
fruit
number
animal
>>> for k in favorites:
print(favorites[k])
apples
42
cats
键和值可能以不同的顺序打印出来,因为与列表不同,字典是无序的。列表listStuff
中的第一项将是listStuff[0]
。但是字典中没有第一项,因为字典没有任何排序。在这段代码中,Python 只是根据它如何存储字典在内存中来选择顺序,这并不能保证始终相同。
在交互式 shell 中输入以下内容:
>>> favorites1 = {'fruit':'apples', 'number':42, 'animal':'cats'}
>>> favorites2 = {'animal':'cats', 'number':42, 'fruit':'apples'}
>>> favorites1 == favorites2
True
表达式favorites1 == favorites2
的评估结果为True
,因为字典是无序的,如果它们具有相同的键-值对,则被认为是相等的。与此同时,列表是有序的,因此具有不同顺序的相同值的两个列表不相等。要查看区别,请在交互式 shell 中输入以下内容:
>>> listFavs1 = ['apples', 'cats', 42]
>>> listFavs2 = ['cats', 42, 'apples']
>>> listFavs1 == listFavs2
False
表达式listFavs1 == listFavs2
的评估结果为False
,因为列表的内容排序不同。
字典有两个有用的方法,keys()
和values()
。它们将分别返回类型为dict_keys
和dict_values
的值。与范围对象一样,这些数据类型的列表形式由list()
返回。
在交互式 shell 中输入以下内容:
>>> favorites = {'fruit':'apples', 'animal':'cats', 'number':42}
>>> list(favorites.keys())
['fruit', 'number', 'animal']
>>> list(favorites.values())
['apples', 42, 'cats']
使用keys()
或values()
方法与list()
一起,您可以获得字典的键或值的列表。
让我们更改新 Hangman 游戏中的代码,以支持不同的秘密单词集。首先,将分配给words
的值替换为一个键为字符串,值为字符串列表的字典。字符串方法split()
将返回一个包含一个单词的字符串列表。
words = {'Colors':'red orange yellow green blue indigo violet white black
brown'.split(),
'Shapes':'square triangle rectangle circle ellipse rhombus trapezoid
chevron pentagon hexagon septagon octagon'.split(),
'Fruits':'apple orange lemon lime pear watermelon grape grapefruit cherry
banana cantaloupe mango strawberry tomato'.split(),
'Animals':'bat bear beaver cat cougar crab deer dog donkey duck eagle
fish frog goat leech lion lizard monkey moose mouse otter owl panda
python rabbit rat shark sheep skunk squid tiger turkey turtle weasel
whale wolf wombat zebra'.split()}
第 48 到 51 行仍然只是一个赋值语句。指令直到第 51 行的最后一个大括号结束。
random
模块中的choice()
函数接受一个列表参数并从中返回一个随机值。这类似于以前的getRandomWord()
函数所做的。您将在getRandomWord()
函数的新版本中使用choice()
。
要了解choice()
函数的工作原理,请在交互式 shell 中输入以下内容:
>>> import random
>>> random.choice(['cat', 'dog', 'mouse'])
'mouse'
>>> random.choice(['cat', 'dog', 'mouse'])
'cat'
就像randint()
函数每次返回一个随机整数一样,choice()
函数从列表中返回一个随机值。
更改getRandomWord()
函数,使其参数成为字符串列表的字典,而不仅仅是字符串列表。以下是该函数最初的样子:
def getRandomWord(wordList):
# This function returns a random string from the passed list of
strings.
wordIndex = random.randint(0, len(wordList) - 1)
return wordList[wordIndex]
更改此函数中的代码,使其看起来像这样:
def getRandomWord(wordDict):
# This function returns a random string from the passed dictionary of
lists of strings and its key.
# First, randomly select a key from the dictionary:
wordKey = random.choice(list(wordDict.keys()))
# Second, randomly select a word from the key's list in the
dictionary:
wordIndex = random.randint(0, len(wordDict[wordKey]) - 1)
return [wordDict[wordKey][wordIndex], wordKey]
我们已将wordList
参数的名称更改为wordDict
,以使其更具描述性。现在,函数首先通过调用random.choice()
从wordDict
字典中选择一个随机键,而不是从字符串列表中选择一个随机单词。而且,函数返回的不再是字符串wordList[wordIndex]
,而是一个包含两个项目的列表。第一个项目是wordDict[wordKey][wordIndex]
。第二个项目是wordKey
。
第 61 行的wordDict[wordKey][wordIndex]
表达式可能看起来很复杂,但它只是一个你可以一步一步评估的表达式,就像其他任何东西一样。首先,想象一下wordKey
的值是'Fruits'
,wordIndex
的值是5
。这是wordDict[wordKey][wordIndex]
的评估方式:
在这种情况下,该函数返回的列表中的项目将是字符串'watermelon'
。(请记住,索引从0
开始,因此[5]
指的是列表中的第六个项目,而不是第五个。)
因为getRandomWord()
函数现在返回的是一个包含两个项目的列表,而不是一个字符串,所以secretWord
将被分配一个列表,而不是一个字符串。您可以使用多重赋值将这两个项目分配到两个单独的变量中,我们将在“多重赋值”中进行介绍。
del
语句将从列表中删除特定索引处的项目。因为del
是一个语句,而不是一个函数或运算符,所以它没有括号,也不会求值为返回值。要尝试它,请在交互式 shell 中输入以下内容:
>>> animals = ['aardvark', 'anteater', 'antelope', 'albert']
>>> del animals[1]
>>> animals
['aardvark', 'antelope', 'albert']
注意,当你删除索引为1
的项目时,原来在索引2
的项目成为了新的索引1
的值;原来在索引3
的项目成为了新的索引2
的值;依此类推。删除的项目上面的所有项目都向下移动了一个索引。
你可以一遍又一遍地输入del animals[1]
来不断删除列表中的项目:
>>> animals = ['aardvark', 'anteater', 'antelope', 'albert']
>>> del animals[1]
>>> animals
['aardvark', 'antelope', 'albert']
>>> del animals[1]
>>> animals
['aardvark', 'albert']
>>> del animals[1]
>>> animals
['aardvark']
HANGMAN_PICS
列表的长度也是玩家的猜测次数。通过从该列表中删除字符串,你可以减少猜测次数,使游戏变得更加困难。
在程序的print('H A N G M A N')
和missedLetters = ''
之间添加以下代码行:
print('H A N G M A N')
difficulty = 'X'
while difficulty not in 'EMH':
print('Enter difficulty: E - Easy, M - Medium, H - Hard')
difficulty = input().upper()
if difficulty == 'M':
del HANGMAN_PICS[8]
del HANGMAN_PICS[7]
if difficulty == 'H':
del HANGMAN_PICS[8]
del HANGMAN_PICS[7]
del HANGMAN_PICS[5]
del HANGMAN_PICS[3]
missedLetters = ''
这段代码从HANGMAN_PICS
列表中删除项目,根据所选的难度级别使其变短。难度级别增加时,HANGMAN_PICS
列表中会删除更多的项目,导致猜测次数减少。Hangman 游戏中的其余代码使用此列表的长度来判断玩家是否已经用完了猜测次数。
多重赋值是一种在一行代码中为多个变量赋值的快捷方式。要使用多重赋值,用逗号分隔变量,并将它们赋给一个值列表。例如,输入以下内容到交互式 shell 中:
>>> spam, eggs, ham = ['apples', 'cats', 42]
>>> spam
'apples'
>>> eggs
'cats'
>>> ham
42
前面的例子等同于以下赋值语句:
>>> spam = ['apples', 'cats', 42][0]
>>> eggs = ['apples', 'cats', 42][1]
>>> ham = ['apples', 'cats', 42][2]
你必须在=
赋值运算符的左侧放置与右侧列表中的项目数量相同的变量。Python 会自动将列表中的第一个项目的值赋给第一个变量,第二个项目的值赋给第二个变量,依此类推。如果变量和项目的数量不一样,Python 解释器会给出错误,如下所示:
>>> spam, eggs, ham, bacon = ['apples', 'cats', 42, 10, 'hello']
Traceback (most recent call last):
File "<pyshell#8>", line 1, in <module>
spam, eggs, ham, bacon = ['apples', 'cats', 42, 10, 'hello']
ValueError: too many values to unpack
>>> spam, eggs, ham, bacon = ['apples', 'cats']
Traceback (most recent call last):
File "<pyshell#9>", line 1, in <module>
spam, eggs, ham, bacon = ['apples', 'cats']
ValueError: need more than 2 values to unpack
更改 Hangman 代码的第 120 行和第 157 行,使用getRandomWord()
的返回值进行多重赋值:
correctLetters = ''
secretWord, secretSet = getRandomWord(words)
gameIsDone = False
--snip--
gameIsDone = False
secretWord, secretSet = getRandomWord(words)
else:
break
第 120 行将getRandomWord(words)
返回的两个值分配给secretWord
和secretSet
。如果玩家选择再玩一局,第 157 行会再次执行这个操作。
你将要做的最后一个更改是告诉玩家他们正在尝试猜测哪个单词集。这样,玩家就会知道秘密单词是动物、颜色、形状还是水果。以下是原始代码:
while True:
displayBoard(missedLetters, correctLetters, secretWord)
在你的新版 Hangman 中,添加第 124 行,使你的程序看起来像这样:
while True:
print('The secret word is in the set: ' + secretSet)
displayBoard(missedLetters, correctLetters, secretWord)
现在你已经完成了对 Hangman 程序的更改。秘密单词不再只是一个字符串列表,而是从许多不同的字符串列表中选择的。程序还会告诉玩家秘密单词来自哪个单词集。尝试玩这个新版本。你可以很容易地在第 48 行开始更改words
字典,以包含更多的单词集。
我们已经完成了 Hangman!在本章中添加额外功能时,你学到了一些新概念。即使在完成编写游戏之后,你也可以随着对 Python 编程的更多了解,随时添加更多功能。
字典与列表类似,不同之处在于它们可以使用任何类型的值作为索引,而不仅仅是整数。字典中的索引称为键。多重赋值是一种快捷方式,用于将多个变量赋值为列表中的值。
与本书中之前的游戏相比,Hangman 相当先进。但是在这一点上,你已经掌握了编写程序的大部分基本概念:变量、循环、函数以及列表和字典等数据类型。本书后面的程序仍然会是一个挑战,但你已经完成了最陡峭的部分!