跳轉到內容

Python學習/案例研究:文字遊戲

來自 Wikibooks,開放世界中的開放書籍

讀取單詞列表

[編輯 | 編輯原始碼]

本章的練習需要一個英語單詞列表。網路上有許多單詞列表可用,但最適合我們目的的是 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
練習 1

編寫一個程式,讀取'words.txt'並僅列印超過 20 個字元的單詞(不包括空格)。

下一節中提供了這些練習的解答。在閱讀解答之前,您應該至少嘗試完成每個練習。

練習 2

1939 年,歐內斯特·文森特·賴特出版了一部名為《Gadsby》的 50,000 字的小說,其中不包含字母“e”。由於“e”是英語中最常見的字母,因此這並不容易做到。

事實上,在不使用這個最常見的符號的情況下構建一個獨立的想法是困難的。一開始進展緩慢,但透過謹慎和數小時的訓練,您可以逐漸獲得熟練程度。

好吧,我現在停下來了。

編寫一個名為has_no_e的函式,如果給定的單詞中不包含字母“e”,則返回'True'

修改您上一節中的程式,使其僅列印不包含“e”的單詞,並計算列表中不包含“e”的單詞的百分比。

練習 3

編寫一個名為'avoids'的函式,該函式接受一個單詞和一個禁止字母的字串,如果該單詞不使用任何禁止字母,則返回'True'

修改您的程式,提示使用者輸入一個禁止字母的字串,然後列印不包含任何禁止字母的單詞的數量。您能否找到 5 個禁止字母的組合,從而排除最少數量的單詞?

練習 4

編寫一個名為uses_only的函式,該函式接受一個單詞和一個字母字串,如果該單詞僅包含列表中的字母,則返回'True'。您能否僅使用字母'acefhlo'造一個句子?除了“Hoe alfalfa?”
練習 5

編寫一個名為uses_all的函式,該函式接受一個單詞和一個必需字母的字串,如果該單詞至少使用一次所有必需字母,則返回'True'。有多少個單詞使用了所有母音'aeiou'?'aeiouy'呢?
練習 6

編寫一個名為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運算子更簡潔地編寫此函式,但我從這個版本開始,因為它演示了搜尋模式的邏輯。

avoidshas_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'中檢視我的解決方案。

  1. wikipedia.org/wiki/Moby_Project
  2. www.cartalk.com/content/puzzler/transcripts/200725
  3. www.cartalk.com/content/puzzler/transcripts/200803
  4. www.cartalk.com/content/puzzler/transcripts/200813
華夏公益教科書