← 学习库 Think Python (2e) · 中英对照 目录

Chapter 9  Case study: word play 第 9 章 案例研究:文字游戏

本页译自 Think Python 2e(Allen B. Downey)· Chapter 9 Case study: word play。代码块保留英文原文不翻译;正文段段对照,中文块可用右下角按钮隐藏。

9.1 Reading word lists 9.1 读词表

For the exercises in this chapter we need a list of English words. There are lots of word lists available on the Web, but the one most suitable for our purpose is one of the word lists collected and contributed to the public domain by Grady Ward as part of the Moby lexicon project (see http://wikipedia.org/wiki/Moby_Project). It is a list of 113,809 official crosswords; that is, words that are considered valid in crossword puzzles and other word games. In the Moby collection, the filename is 113809of.fic; you can download a copy, with the simpler name words.txt, from http://thinkpython.com/code/words.txt.

本章的习题需要一份英语单词表。网上的词表很多,但最适合我们的是 Grady Ward 作为 Moby 词库项目的一部分收集并贡献到公共领域的那一份(见 http://wikipedia.org/wiki/Moby_Project)。它收录了 113,809 个「正式填字词」,也就是在填字游戏和其他文字游戏里被认可的词。在 Moby 词库中,这个文件叫 113809of.fic;你可以从 http://thinkpython.com/code/words.txt 下载一份改用了更简单名字 words.txt 的副本。

This file is in plain text, so you can open it with a text editor, but you can also read it from Python. The built-in function open takes the name of the file as a parameter and returns a file object you can use to read the file.

这个文件是纯文本,你可以用文本编辑器打开它,也可以从 Python 里读取。内置函数 open 接受文件名作为形参,返回一个文件对象,你可以用它来读文件。
open00014

fin is a common name for a file object used for input. Mode fin indicates that this file is open for reading (as opposed to fin for writing).

fin 是用于输入的文件对象的常用名字。模式 fin 表示这个文件以读取方式打开(对应地,fin 表示写入)。

The file object provides several methods for reading, including open0002, which reads characters from the file until it gets to a newline and returns the result as a string:

文件对象提供了若干读取方法,其中 open0002 会从文件里一直读字符,直到遇到换行符,并把结果作为字符串返回:
open00023

The first word in this particular list is “aa,” which is a kind of lava. The sequence open represents two whitespace characters, a carriage return and a newline, that separate this word from the next.

这份词表里的第一个词是「aa」,一种熔岩。序列 open 代表两个空白字符——回车和换行——它们把这个词和下一个词隔开。

The file object keeps track of where it is in the file, so if you call open0002 again, you get the next word:

文件对象会记住自己在文件中的位置,所以再调一次 open0002,你就拿到下一个词:
open00028

The next word is “aah,” which is a perfectly legitimate word, so stop looking at me like that. Or, if it’s the whitespace that’s bothering you, we can get rid of it with the string method open0:

下一个词是「aah」,这是个完全合法的单词,所以别那样看着我。要是让你不舒服的其实是那些空白字符,我们可以用字符串方法 open0 把它们去掉:
open00031

You can also use a file object as part of a fin loop. This program reads open00033 and prints each word, one per line:

文件对象还能直接用在 fin 循环里。下面这个程序读取 open00035,每行打印一个词:
open00036

Exercise 1

习题 1

Write a program that reads open00037 and prints only the words with more than 20 characters (not counting whitespace).

写一个程序,读取 open00038,只打印长度超过 20 个字符的单词(不计空白字符)。

9.2 Exercises 9.2 习题

There are solutions to these exercises in the next section. You should at least attempt each one before you read the solutions.

下一节给出了这些习题的解答。看解答之前,每道题你至少都该先试一试。

Exercise 2

习题 2

In 1939 Ernest Vincent Wright published a 50,000 word novel called Gadsby that does not contain the letter “e.” Since “e” is the most common letter in English, that’s not easy to do.

1939 年,Ernest Vincent Wright 出版了一部 5 万词的小说《Gadsby》,全书不含字母「e」。「e」是英语中最常见的字母,做到这一点可不容易。

In fact, it is difficult to construct a solitary thought without using that most common symbol. It is slow going at first, but with caution and hours of training you can gradually gain facility.

事实上,不用这个最常见的符号,连一个念头都难以组织成句。起初进展缓慢,但只要小心谨慎,加上数小时的训练,你也能渐渐上手。(说明:这两段英文原文本身也一个「e」都没有。)

All right, I’ll stop now.

好啦,我就此打住。

Write a function called open0003 that returns open if the given word doesn’t have the letter “e” in it.

写一个名为 open0004 的函数,如果给定单词里没有字母「e」,就返回 open

Modify your program from the previous section to print only the words that have no “e” and compute the percentage of the words in the list have no “e.”

修改上一节的程序,只打印不含「e」的单词,并算出词表中不含「e」的单词所占的百分比。

Exercise 3

习题 3

Write a function named open00 that takes a word and a string of forbidden letters, and that returns open if the word doesn’t use any of the forbidden letters.

写一个名为 open00 的函数,接受一个单词和一串禁用字母,如果这个单词没有用到任何禁用字母,就返回 open

Modify your program to prompt the user to enter a string of forbidden letters and then print the number of words that don’t contain any of them. Can you find a combination of 5 forbidden letters that excludes the smallest number of words?

修改你的程序,提示用户输入一串禁用字母,然后打印不含其中任何字母的单词数量。你能找出一组 5 个禁用字母,使被排除的单词数量最少吗?

Exercise 4

习题 4

Write a function named open00047 that takes a word and a string of letters, and that returns open if the word contains only letters in the list. Can you make a sentence using only the letters open000? Other than “Hoe alfalfa?”

写一个名为 open00050 的函数,接受一个单词和一串字母,如果这个单词只用到了表中的字母,就返回 open。你能只用字母 open000 造出一句话吗?除了「Hoe alfalfa?」(锄紫花苜蓿?)之外的那种。

Exercise 5

习题 5

Write a function named open0005 that takes a word and a string of required letters, and that returns open if the word uses all the required letters at least once. How many words are there that use all the vowels open0? How about open00?

写一个名为 open0005 的函数,接受一个单词和一串必需字母,如果这个单词把每个必需字母都至少用了一次,就返回 open。有多少单词用到了全部元音 open0?换成 open00 呢?

Exercise 6

习题 6

Write a function called open00061 that returns open if the letters in a word appear in alphabetical order (double letters are ok). How many abecedarian words are there?

写一个名为 open00063 的函数,如果一个单词里的字母按字母表顺序出现,就返回 open(允许连续重复的字母)。这样的「字母序单词」有多少个?

9.3 Search 9.3 搜索

All of the exercises in the previous section have something in common; they can be solved with the search pattern we saw in Section 8.6. The simplest example is:

上一节的习题有个共同点:它们都能用 8.6 节见过的搜索模式来解决。最简单的例子是:
open00065

The fin loop traverses the characters in open. If we find the letter “e”, we can immediately return open0; otherwise we have to go to the next letter. If we exit the loop normally, that means we didn’t find an “e”, so we return open.

fin 循环遍历 open 中的字符。一旦找到字母「e」,就可以立即返回 open0;否则继续看下一个字母。如果循环正常结束,说明没找到「e」,于是返回 open

open00 is a more general version of open0007 but it has the same structure:

open00 是 open0007 的通用版本,但结构完全相同:
open00078

We can return open0 as soon as we find a forbidden letter; if we get to the end of the loop, we return open.

一找到禁用字母就可以返回 open0;如果走到循环末尾,就返回 open

open00083 is similar except that the sense of the condition is reversed:

open00084 与之类似,只是条件的方向反了过来:
open00085

Instead of a list of forbidden letters, we have a list of available letters. If we find a letter in open that is not in open00087, we can return open0.

这次给的不是禁用字母表,而是可用字母表。只要在 open 里发现一个不在 open00090 中的字母,就可以返回 open0。

open0009 is similar except that we reverse the role of the word and the string of letters:

open0009 也类似,只是把单词和字母串的角色调换了:
open00094

Instead of traversing the letters in open, the loop traverses the required letters. If any of the required letters do not appear in the word, we can return open0.

循环遍历的不再是 open 里的字母,而是那些必需字母。只要有任何一个必需字母没在单词里出现,就可以返回 open0。

If you were really thinking like a computer scientist, you would have recognized that open0009 was an instance of a previously-solved problem, and you would have written:

如果你真的像计算机科学家那样思考,就会认出 open0010 只是一个已解决问题的实例,于是会这样写:
open00101

This is an example of a program development method called problem recognition, which means that you recognize the problem you are working on as an instance of a previously-solved problem, and apply a previously-developed solution.

这就是一种叫做问题识别的程序开发方法:你认出手头的问题是某个已解决问题的实例(即归约到已解决的问题),于是直接套用现成的解法。

9.4 Looping with indices 9.4 带索引的循环

I wrote the functions in the previous section with fin loops because I only needed the characters in the strings; I didn’t have to do anything with the indices.

上一节的函数我都用 fin 循环写,因为我只需要字符串里的字符,用不着跟索引打交道。

For open00104 we have to compare adjacent letters, which is a little tricky with a fin loop:

open00106 要比较相邻的字母,用 fin 循环写就有点别扭:
open00108

An alternative is to use recursion:

另一种办法是用递归:
open00109

Another option is to use a open0 loop:

还有一种选择是用 open0 循环:
open00112

The loop starts at fin and ends when open00114. Each time through the loop, it compares the ith character (which you can think of as the current character) to the i+1th character (which you can think of as the next).

循环从 fin 开始,到 open00116 时结束。每走一遍循环,就把第 i 个字符(可以看作当前字符)和第 i+1 个字符(可以看作下一个字符)作比较。

If the next character is less than (alphabetically before) the current one, then we have discovered a break in the abecedarian trend, and we return open0.

如果下一个字符小于(按字母表排在前面)当前字符,就说明字母序被打断了,于是返回 open0。

If we get to the end of the loop without finding a fault, then the word passes the test. To convince yourself that the loop ends correctly, consider an example like open0011. The length of the word is 6, so the last time the loop runs is when i is 4, which is the index of the second-to-last character. On the last iteration, it compares the second-to-last character to the last, which is what we want.

如果一路走到循环末尾都没发现问题,这个单词就通过了检验。想确认循环的结束条件没写错,可以拿 open0012 举例:词长为 6,所以循环最后一次执行时 i 是 4,正是倒数第二个字符的索引。最后这一轮比较的是倒数第二个字符和最后一个字符,正合我们的意。

Here is a version of open00123 (see Exercise 6) that uses two indices; one starts at the beginning and goes up; the other starts at the end and goes down.

下面是 open00124 的一个版本(见习题 6),它用了两个索引:一个从开头往后走,另一个从末尾往前走。
open00125

Or, if you noticed that this is an instance of a previously-solved problem, you might have written:

或者,如果你注意到这也是一个已解决问题的实例,可能就会这样写:
open00126

Assuming you did Exercise 9.

当然,前提是你做过习题 9。

9.5 Debugging 9.5 调试

Testing programs is hard. The functions in this chapter are relatively easy to test because you can check the results by hand. Even so, it is somewhere between difficult and impossible to choose a set of words that test for all possible errors.

测试程序是件难事。本章的函数还算容易测,因为结果可以手工核对。即便如此,想挑出一组单词覆盖所有可能的错误,难度介于「很难」和「不可能」之间。

Taking open0012 as an example, there are two obvious cases to check: words that have an ’e’ should return open0; words that don’t should return open. You should have no trouble coming up with one of each.

open0013 来说,有两种显而易见的情形要检查:含「e」的词应返回 open0,不含的应返回 open。各举一例不难。

Within each case, there are some less obvious subcases. Among the words that have an “e,” you should test words with an “e” at the beginning, the end, and somewhere in the middle. You should test long words, short words, and very short words, like the empty string. The empty string is an example of a special case, which is one of the non-obvious cases where errors often lurk.

而每种情形之下,还藏着一些不那么显眼的子情形。在含「e」的词里,你该分别测「e」在开头、在末尾、在中间某处的情况;该测长词、短词,以及非常短的词,比如空字符串。空字符串就是特例的一个例子——这类不显眼的情形里往往潜伏着错误。

In addition to the test cases you generate, you can also test your program with a word list like open00133. By scanning the output, you might be able to catch errors, but be careful: you might catch one kind of error (words that should not be included, but are) and not another (words that should be included, but aren’t).

除了自己造的测试用例,你还可以拿 open00134 这样的词表来测。扫一眼输出也许能抓到错误,但要小心:你可能抓到了一类错误(本不该收进来却收了的词),却漏掉另一类(本该收进来却没收的词)。

In general, testing can help you find bugs, but it is not easy to generate a good set of test cases, and even if you do, you can’t be sure your program is correct.

总的说来,测试能帮你找到缺陷,但要造出一组好的测试用例并不容易;就算造出来了,你也不能确定程序就是对的。

According to a legendary computer scientist:

一位传奇计算机科学家说过:

Program testing can be used to show the presence of bugs, but never to show their absence!

程序测试能用来证明缺陷的存在,却永远无法证明缺陷不存在!

— Edsger W. Dijkstra

—— Edsger W. Dijkstra

9.6 Glossary 9.6 术语表

file object:
A value that represents an open file.
problem recognition:
A way of solving a problem by expressing it as an instance of a previously-solved problem.
special case:
A test case that is atypical or non-obvious (and less likely to be handled correctly).
file object 文件对象:
代表一个已打开文件的值。
problem recognition 问题识别:
一种解题方式:把当前问题表述为某个已解决问题的实例,即归约到已解决的问题。
special case 特例:
不典型或不显眼的测试用例(也因此更容易被处理错)。

9.7 Exercises 9.7 习题

Exercise 7

习题 7

This question is based on a Puzzler that was broadcast on the radio program Car Talk (open00135):

本题取自广播节目《Car Talk》播出的一道谜题(open00136):

Give me a word with three consecutive double letters. I’ll give you a couple of words that almost qualify, but don’t. For example, the word committee, c-o-m-m-i-t-t-e-e. It would be great except for the ‘i’ that sneaks in there. Or Mississippi: M-i-s-s-i-s-s-i-p-p-i. If you could take out those i’s it would work. But there is a word that has three consecutive pairs of letters and to the best of my knowledge this may be the only word. Of course there are probably 500 more but I can only think of one. What is the word?

给我一个含有三组连续双写字母的单词。我先举两个差一点就合格、但其实不合格的例子。比如 committee,c-o-m-m-i-t-t-e-e,要不是中间偷偷冒出个「i」,它就完美了。再比如 Mississippi:M-i-s-s-i-s-s-i-p-p-i,把那几个「i」抽掉就行了。但确实有个单词含有三组紧挨着的双写字母,据我所知它可能是唯一的一个。当然也许还有五百个,只不过我只想得出一个。那个词是什么?

Write a program to find it. Solution: open00137.

写个程序把它找出来。解答:open00138。

Exercise 8 Here’s another Car Talk Puzzler (open00139):

习题 8 又一道《Car Talk》谜题(open00140):

“I was driving on the highway the other day and I happened to notice my odometer. Like most odometers, it shows six digits, in whole miles only. So, if my car had 300,000 miles, for example, I’d see 3-0-0-0-0-0.

「前几天我在高速上开车,无意间瞥了一眼里程表。和大多数里程表一样,它显示六位数字,只记整英里。所以,假如我的车跑了 300,000 英里,我看到的就是 3-0-0-0-0-0。

“Now, what I saw that day was very interesting. I noticed that the last 4 digits were palindromic; that is, they read the same forward as backward. For example, 5-4-4-5 is a palindrome, so my odometer could have read 3-1-5-4-4-5.

「那天我看到的可有意思了。我注意到最后 4 位是回文的,也就是正着读和倒着读一样。比如 5-4-4-5 就是个回文,所以我的里程表可能显示 3-1-5-4-4-5。

“One mile later, the last 5 numbers were palindromic. For example, it could have read 3-6-5-4-5-6. One mile after that, the middle 4 out of 6 numbers were palindromic. And you ready for this? One mile later, all 6 were palindromic!

「又开了一英里,最后 5 位成了回文。比如它可能显示 3-6-5-4-5-6。再过一英里,6 位中间的那 4 位是回文。准备好了吗?再过一英里,6 位全都是回文!

“The question is, what was on the odometer when I first looked?”

「问题是:我第一眼看到时,里程表上是多少?」

Write a Python program that tests all the six-digit numbers and prints any numbers that satisfy these requirements. Solution: open00141.

写一个 Python 程序,检验所有六位数,打印出满足这些条件的数。解答:open00142。

Exercise 9 Here’s another Car Talk Puzzler you can solve with a search (open00143):

习题 9 再来一道能用搜索解决的《Car Talk》谜题(open00144):

“Recently I had a visit with my mom and we realized that the two digits that make up my age when reversed resulted in her age. For example, if she’s 73, I’m 37. We wondered how often this has happened over the years but we got sidetracked with other topics and we never came up with an answer.

「最近我去看我妈,我们发现我年龄的两位数字倒过来正好是她的年龄。比如她 73 岁,我就 37 岁。我们琢磨这些年来这种情况出现过几次,可聊着聊着就跑题了,一直没算出答案。

“When I got home I figured out that the digits of our ages have been reversible six times so far. I also figured out that if we’re lucky it would happen again in a few years, and if we’re really lucky it would happen one more time after that. In other words, it would have happened 8 times over all. So the question is, how old am I now?”

「回到家我算出来,到目前为止我们俩年龄的数字互为倒序已经出现过六次。我还算出,运气好的话过几年还会再来一次;运气特别好的话,那之后还能再来一次。也就是说,总共会出现 8 次。那么问题是:我现在多大?」

Write a Python program that searches for solutions to this Puzzler. Hint: you might find the string method open0 useful.

写一个 Python 程序来搜索这道谜题的解。提示:字符串方法 open0 也许用得上。

Solution: open00147.

解答:open00148。