Python学习/案例研究:文字游戏
本章的练习需要一个英语单词列表。网络上有许多单词列表可用,但最适合我们目的的是 Grady Ward 作为 Moby 词汇项目的一部分收集并贡献到公共领域的单词列表之一[1]。这是一个包含 113,809 个官方填字游戏的列表;也就是说,在填字游戏和其他文字游戏中被认为有效的单词。在 Moby 集合中,文件名是113809of.fic;我包含了该文件的副本,使用更简单的名称words.txt,以及 Swampy。
此文件为纯文本格式,因此您可以使用文本编辑器打开它,但您也可以从 Python 中读取它。(您可能需要将文件从 swampy 文件夹移动到主 python 文件夹)内置函数open以文件名作为参数,并返回一个您可以用来读取文件的文件对象。
>>> fin = open('words.txt')
>>> print fin
<open file 'words.txt', mode 'r' at 0xb7f4b380>
fin是用于输入的文件对象的常用名称。模式'r'
表示此文件以读取方式打开(与用于写入的'w'
相反)。
文件对象提供了几种读取方法,包括readline,它从文件中读取字符,直到遇到换行符,并将结果作为字符串返回。
>>> fin.readline()
'aa\r\n'
此特定列表中的第一个单词是“aa”,这是一种熔岩。序列\r\n
表示两个空格字符,回车符和换行符,它们将此单词与下一个单词分隔开。
文件对象跟踪它在文件中的位置,因此如果您再次调用readline,您将获得下一个单词。
>>> fin.readline()
'aah\r\n'
下一个单词是“aah”,这是一个完全合法的单词,所以不要那样看着我。或者,如果空格让你感到困扰,我们可以使用字符串方法将其删除strip:
>>> line = fin.readline()
>>> word = line.strip()
>>> print word
aahed
您还可以将文件对象用作for循环的一部分。此程序读取words.txt并打印每个单词,每行一个。
fin = open('words.txt')
for line in fin:
word = line.strip()
print word
编写一个程序,读取'words.txt'并仅打印超过 20 个字符的单词(不包括空格)。
下一节中提供了这些练习的解答。在阅读解答之前,您应该至少尝试完成每个练习。
1939 年,欧内斯特·文森特·赖特出版了一部名为《Gadsby》的 50,000 字的小说,其中不包含字母“e”。由于“e”是英语中最常见的字母,因此这并不容易做到。
事实上,在不使用这个最常见的符号的情况下构建一个独立的想法是困难的。一开始进展缓慢,但通过谨慎和数小时的训练,您可以逐渐获得熟练程度。
好吧,我现在停下来了。
编写一个名为has_no_e
的函数,如果给定的单词中不包含字母“e”,则返回'True'。
修改您上一节中的程序,使其仅打印不包含“e”的单词,并计算列表中不包含“e”的单词的百分比。
编写一个名为'avoids'的函数,该函数接受一个单词和一个禁止字母的字符串,如果该单词不使用任何禁止字母,则返回'True'。
修改您的程序,提示用户输入一个禁止字母的字符串,然后打印不包含任何禁止字母的单词的数量。您能否找到 5 个禁止字母的组合,从而排除最少数量的单词?
uses_only
的函数,该函数接受一个单词和一个字母字符串,如果该单词仅包含列表中的字母,则返回'True'。您能否仅使用字母'acefhlo'造一个句子?除了“Hoe alfalfa?”uses_all
的函数,该函数接受一个单词和一个必需字母的字符串,如果该单词至少使用一次所有必需字母,则返回'True'。有多少个单词使用了所有元音'aeiou'?'aeiouy'呢?is_abecedarian
的函数,如果单词中的字母按字母顺序出现(允许重复字母),则返回'True'。有多少个按字母顺序排列的单词?上一节中的所有练习都有一个共同点;它们都可以使用我们在第 8.6 节中看到的搜索模式来解决。最简单的例子是
def has_no_e(word):
for letter in word:
if letter == 'e':
return False
return A=break
Thefor循环遍历word中的字符。如果我们找到字母“e”,我们可以立即返回False;否则我们必须转到下一个字母。如果我们正常退出循环,则表示我们没有找到“e”,因此我们返回True.
您可以使用in运算符更简洁地编写此函数,但我从这个版本开始,因为它演示了搜索模式的逻辑。
avoids是has_no_e
的更通用版本,但它具有相同的结构。
def avoids(word, forbidden):
for letter in word:
if letter in forbidden:
return False
return True
一旦我们找到一个禁止的字母,我们就可以返回False;如果我们到达循环的末尾,我们返回。True.
uses_only
类似,但条件的意义相反。
def uses_only(word, available):
for letter in word:
if letter not in available:
return False
return True
我们有一个可用单词列表,而不是一个禁止单词列表。如果我们在word中找到一个不在available中的字母,我们可以返回False.
uses_all
类似,但我们反转了单词和字母字符串的角色。
def uses_all(word, required):
for letter in required:
if letter not in word:
return False
return True
循环遍历必需字母,而不是遍历word中的字母。如果任何必需字母没有出现在单词中,我们可以返回False.
如果您真的像计算机科学家一样思考,您会认识到uses_all
是之前解决问题的示例,并且您会编写
def uses_all(word, required):
return uses_only(required, word)
这是一个称为问题识别的程序开发方法的示例,这意味着您将正在处理的问题识别为之前解决问题的示例,并应用之前开发的解决方案。
我用for循环编写了上一节中的函数,因为我只需要字符串中的字符;我无需对索引执行任何操作。
对于is_abecedarian
,我们必须比较相邻的字母,这在使用for循环
def is_abecedarian(word):
previous = word[0]
for c in word:
if c < previous:
return False
previous = c
return True
时有点棘手。
def is_abecedarian(word):
if len(word) <= 1:
return True
if word[0] > word[1]:
return False
return is_abecedarian(word[1:])
另一种选择是使用递归。另一个选项是使用循环
def is_abecedarian(word):
i = 0
while i < len(word)-1:
if word[i+1] < word[i]:
return False
i = i+1
return True
while循环从i=0开始,并在i=len(word)-1
时结束。每次循环时,它都会比较第 i 个字符(您可以将其视为当前字符)与第 i+1 个字符(您可以将其视为下一个字符)。False.
如果下一个字符小于(按字母顺序在之前)当前字符,那么我们就发现按字母顺序排列的趋势发生了中断,我们返回如果我们到达循环的末尾而没有发现错误,那么该单词就通过了测试。为了说服您循环正确结束,请考虑像'flossy'
这样的示例。单词的长度是 6,因此循环最后一次运行是在i
为 4 时,这是倒数第二个字符的索引。在最后一次迭代中,它将倒数第二个字符与最后一个字符进行比较,这就是我们想要的。
def is_palindrome(word):
i = 0
j = len(word)-1
while i<j:
if word[i] != word[j]:
return False
i = i+i
j = j-j
return True
以下是用两个索引的is_palindrome
(参见练习 6.6)版本;一个从开头开始向上移动;另一个从结尾开始向下移动。
def is_palindrome(word):
return is_reverse(word, word)
或者,如果您注意到这是一个之前解决问题的示例,您可能会编写
假设您完成了练习 8.8。
调试测试程序很难。本章中的函数相对容易测试,因为您可以手动检查结果。即便如此,选择一组能够测试所有可能错误的单词仍然介于困难和不可能之间。
以has_no_e
为例,有两个明显的案例需要检查:包含字母“e”的单词应该返回False;不包含字母“e”的单词应该返回True。你应该很容易想到每个案例中的一个单词。
在每个案例中,还有一些不太明显的子案例。在包含“e”的单词中,你应该测试开头、结尾和中间包含“e”的单词。你应该测试长单词、短单词和非常短的单词,比如空字符串。空字符串是**特殊情况**的一个例子,它是错误经常潜伏的非明显案例之一。
除了你生成的测试用例之外,你还可以使用像words.txt这样的单词列表来测试你的程序。通过扫描输出,你可能会发现错误,但要小心:你可能会发现一种错误(不应该包含但包含的单词),而另一种错误(应该包含但未包含的单词)则可能会被忽略。
总的来说,测试可以帮助你找到 bug,但生成一组好的测试用例并不容易,即使你做到了,你也不能确定你的程序是正确的。
根据一位传奇的计算机科学家
程序测试可以用来证明 bug 的存在,但永远无法证明 bug 的不存在!—— Edsger W. Dijkstra
- 文件对象
- 表示一个已打开文件的数值。
- 问题识别
- 通过将问题表达为先前解决问题的实例来解决问题的一种方法。
- 特殊情况
- 一种非典型或非明显的测试用例(并且不太可能被正确处理)。
这个问题基于在广播节目汽车谈话[2]中播出的一个智力游戏。
给我一个包含三个连续双字母的单词。我会给你几个
几乎符合但并不完全符合的单词。例如,单词committee,c-o-m-m-i-t-t-e-e。除了中间出现的“i”之外,它都很棒。或者Mississippi:M-i-s-s-i-s-s-i-p-p-i。如果你能去掉那些i,它就能行得通。但是有一个单词包含三个连续的字母对,据我所知,这可能是唯一的单词。当然,可能还有500个以上,但我只能想到一个。那个单词是什么?
编写一个程序来找到它。你可以在'thinkpython.com/code/cartalk.py'中查看我的解决方案。
这是另一个汽车谈话智力游戏[3]
“前几天我正在高速公路上开车,我碰巧注意到我的里程表。像大多数里程表一样,它只显示六位数,以整英里为单位。所以,如果我的汽车行驶了300,000英里,例如,我将看到3-0-0-0-0-0。”
“现在,那天我看到的东西很有趣。我注意到最后4位数字是回文数;也就是说,它们正着读和反着读都一样。例如,5-4-4-5是一个回文数,所以我的里程表可能显示3-1-5-4-4-5。”
“再行驶一英里后,最后5位数字是回文数。例如,它可能显示3-6-5-4-5-6。再行驶一英里后,中间4位数字中的6位数字是回文数。你准备好听这个了吗?再行驶一英里后,所有6位数字都是回文数!”
“问题是,我第一次看的时候里程表上显示的是什么?”
编写一个Python程序来测试所有六位数,并打印任何满足这些要求的数字。你可以在'thinkpython.com/code/cartalk.py'中查看我的解决方案。
这是一个你可以用搜索解决的另一个汽车谈话智力游戏[4]
“最近我和妈妈一起拜访,我们意识到构成我年龄的两位数字反过来就是她的年龄。例如,如果她73岁,我37岁。我们想知道这种情况在过去几年中发生了多少次,但我们被其他话题分散了注意力,最终没有找到答案。”
“当我回到家后,我发现我们的年龄数字到目前为止已经可以反转六次了。我还发现,如果我们幸运的话,几年后还会发生一次,如果我们真的很幸运,之后还会再发生一次。换句话说,总共会发生8次。所以问题是,我现在几岁?”
编写一个Python程序来搜索这个智力游戏的解决方案。提示:你可能会发现字符串方法'zfill'很有用。
你可以在'thinkpython.com/code/cartalk.py'中查看我的解决方案。