在上一篇 local vs global 函數的教學裡,我們透過 Python 版本的「猜數字遊戲」介紹如何使用 global
這個 Python 的關鍵字。
除了 global
以外,在這個猜數字遊戲裡其實有更多的 Python 重點。
以下我們一起透過逐步建立這個猜數字遊戲的編程,淺談一些編程設計和處理異常的方法吧!
學習大綱
我們堅信學習編程的最好方法,就是學習如何逐步思考、設計和執行一個問題。在這個猜數字遊戲裡,我們希望重點介紹一下幾點:
- 分析問題:如何設計編程
- 實質編程:製做 MVP
- 處理異常:減低程式出錯機會
- 改良程式:簡化架構(在下一篇教學)
分析問題:如何設計編程
所有編程的起點都是從分析問題開始。我們先回顧一下這個猜數字遊戲的描述:
我們先隨機從 1 到 100 選一個數字(假設這個數字是 75)。我先告訴您這個數字是 1 至 100 中間。如果您猜的是 70(少於答案),我再告訴您這個數字在 70 至 100 中間,如此類推,直至您猜中數字。
當我們看這個描述時,留意以下這些重點:
題目字眼 | 編程要點 |
---|---|
隨機選一個數字 | 透過 Python 的 library 在一個指定的數字範圍裡生成隨機數字 |
如果您猜的是 70 | 使用 < > = 等的邏輯運算(logical operators)比較以下的數字:(1) 用家提供的數字(70) (2) 答案(75) (3) 現在的範圍(0 至 100) |
我再告訴您這個數字在 70 至 100 中間 | 以用家提供的數字更新現在的範圍 |
如此類推 | 使用循環(loop)以不斷要求用家提供數字 |
直至您猜中數字 | 完結循環(loop)的條件時猜中數字 |
透過把我們原本的問題切割成不同的字眼,並連結成編程要點,我們便可以將問題逐一破解了!這就是所謂的分而治之(divide-and-conquer)。
偽代碼(pseudo-code)
要完成設計編程,我們可以把以上的編程要點製成偽代碼(pseudo-code),讓我們可以按著這個架構作基礎開始編程:
# 匯入 library ## 生成隨機數字的 library # 定義常數(constant)及函數(variable) ## 範圍(1 至 100) ## 隨機生成答案(70) # 定義循環(loop) while True: # 要求用家提供一個數字(例如 70) # 比較用家提供的數字及範圍與答案 ## 如果 提供的數字 等於 答案 -> 告訴玩家成功了,並離開循環 ## 如果 提供的數字 小於 答案 -> 將範圍的底設為提供的數字( 0 至 100 修改成 70 至 100) ## 如果 提供的數字 大於 答案 -> 將範圍的底設為提供的數字( 0 至 100 修改成 0 至 70)
在這個偽代碼的例子裡,我們使用編程注解(comments)代替我們將會輸入的實際 Python 代碼。這是為了像填色本一樣,先有了編程輪廓,慢慢填入 Python 代碼。
在輪廓裡,我們把編程分成數個分部(例如「匯入 library」、「定義循環」),提醒我們有哪些必須的編程元素。
我們亦應該提供我們預期的輸出(expected output)。譬如我們使用「如果 小於 -> 結果」的字句,總結我們不同情況下的輸出。
有了這個輪廓,我們便可以開始 Python 編程了!
實質編程:製做 MVP
有了我們偽代碼(pseudo-code),我們便可以開死編寫一個編程 MVP。
MVP 這裡不是指 Most Valuable Player(最有價值球員),而是一個 agile 架構裡的概念:Minimum Viable Product(最簡可行產品)。
如果要非常粗略地定義 MVP,就是建立一個「半桶水」的方案。我們暫時不管用戶體驗、異常處理、最佳化表現等的問題,只放眼達成最基本、簡單的目的。
在我們的例子,就是直接簡單地把偽代碼(文字)轉變成 Python 編程,不加任何其他的元素。
# 匯入 library import random # 定義常數(constant)及函數(variable) min_x = 0 max_x = 100 answer = random.randint(min_x,max_x) # 定義循環(loop) while True: guess = int(input('請從 ' + str(min_x) + ' 到 ' + str(max_x) + ' 猜一個數字:')) # 比較用家提供的數字及範圍與答案 if guess == answer: print('恭喜您猜對了!答案是 ' + str(answer)) break elif guess < answer: min_x = guess elif guess > answer: max_x = guess
在以上編程,我們直接把偽代碼的「##」注解轉為 Python 的代碼。留意我們沒有加入任何其他元素,亦沒有考慮任何編程設計的最佳化(optimization),只是純粹製造出 MVP。
總括而言,這個階段您不應該多想(overthink),只需要把最基本的要求達成。
處理異常:減低程式出錯機會
有了我們的 MVP,我們下一個要處理的問題便是用家體驗。
在編程裡,我們應該假設用家是一個零經驗,完全不懂該怎操作編程的人。特別是一些要求用家提供資料的編程(例如我們要求玩家猜數字),我們不能假設用家只會提供合乎邏輯的資料。
比方說,我們要求用家提供電郵地址時,不能假設用家總會提供「[email protected]」格式。用家可能會提供「A@gmail.」、「1234」等不是電郵地址的輸入。
在我們的猜數字遊戲,我們可以預計用家會「犯下」以下的輸入錯誤:
- 用家輸入的不是數字(例如「abc」)
- 用家輸入超出範圍的數字(例如在 0 至 100 的時候猜 120)
我們逐個問題處理吧!
用家輸入的不是數字
min_x = 0 max_x = 100 answer = 75 guess = int(input('請從 ' + str(min_x) + ' 到 ' + str(max_x) + ' 猜一個數字:')) if guess == answer: print('恭喜您猜對了!答案是 ' + str(answer)) elif guess < answer: min_x = guess elif guess > answer: max_x = guess
從以上抽出來的編程部分可見,如果我們輸入的是「abc」,Python 會出現異常。這是因為我們使用 int()
功能將用家提供的數字轉換成整數(integer)時,如果用家提供的是文字(string),Python 便不能轉換成整數。
所以,一個簡單的方法便是先檢查用家的 input()
是否正整數(positive integer),然後再把數字轉換成整數數據(int type)。
min_x = 0 max_x = 100 answer = 75 guess = input('請從 ' + str(min_x) + ' 到 ' + str(max_x) + ' 猜一個數字:') # 先檢查 guess 是否正整數 if guess.isnumeric(): print(guess + ' 是整數') guess = int(guess) else: print(guess +' 不是整數') if guess == answer: print('恭喜您猜對了!答案是 ' + str(answer)) elif guess < answer: min_x = guess elif guess > answer: max_x = guess
以上使用 guess.isnumeric()
可以讓我們檢查用家提供的資料是否正整數,而我們在 guess.isnumeric()
是真(True)值時才將 guess 轉為整數,便不會出現異常。
問題出現了:當我們只是使用 if 來決定資料是否正確時,如果 guess 不是正整數,根據我們的編程順序,我們不會把資料轉換成正整數,以致以上例子中,出現「TypeError: ‘<‘ not supported between instances of ‘str’ and ‘int’」的錯誤。這是因為 Python 不能告訴您「abc」還是 0 較小。
min_x = 0 max_x = 100 answer = 75 # 先檢查 guess 是否正整數,如果不是,要求用家重新輸入正整數,直到 guess 是一個正整數 while True: guess = input('請從 ' + str(min_x) + ' 到 ' + str(max_x) + ' 猜一個數字:') if guess.isnumeric(): guess = int(guess) break if guess == answer: print('恭喜您猜對了!答案是 ' + str(answer)) elif guess < answer: min_x = guess elif guess > answer: max_x = guess
所以,這裡我們的解決方法便是重複要求用家輸入資料,直到資料可以被轉換成一個正整數。
以上的例子裡,我們使用一個 while True
循環,並指明程式只有 guess.isnumeric()
是真(True)時,才將 guess 轉為整數類型,並結束這一個循環。
如果您在 Google Colab 嘗試運行這段代碼,您會發現不論您輸入甚麼文字、小數點,只有您輸入正整數時程式才會停止要求您輸入資料。
用家輸入超出範圍的數字
另一個可能性是如果用家在我們希望他輸入 0 至 100 的數字時,輸入了 120,在原本的設計裡,我們會在下一個循環(iteration)要求用家輸入 0 至 120 的數字。
這是不正確的資料,因為假如用家不斷地輸入比最大值(upper bound)更高的數字,那麼這個遊戲可不能結束。
而歸根結底,這是因為我們在 MVP 時,更新 min_x 和 max_x 的段落沒有限制 guess 的值,以致用家提供出了範圍(out of bound)的數字時,我們也會更新 min_x 和 max_x。
min_x = 0 max_x = 100 answer = 75 guess = int(input('請從 ' + str(min_x) + ' 到 ' + str(max_x) + ' 猜一個數字:')) if guess == answer: print('恭喜您猜對了!答案是 ' + str(answer)) elif guess < answer: min_x = guess elif guess > answer: max_x = guess guess = int(input('請從 ' + str(min_x) + ' 到 ' + str(max_x) + ' 猜一個數字:'))
解決這個問題相對簡單:我們只需要在更新 min_x 和 max_x 的條件裡進一步限制 guess 的數值,便可以避免這個問題。
min_x = 0 max_x = 100 answer = 75 guess = int(input('請從 ' + str(min_x) + ' 到 ' + str(max_x) + ' 猜一個數字:')) if guess == answer: print('恭喜您猜對了!答案是 ' + str(answer)) # 限制 min_x,max_x 和 guess 的關係 elif (guess < answer) and (min_x < guess): min_x = guess elif (guess > answer) and (max_x > guess): max_x = guess guess = int(input('請從 ' + str(min_x) + ' 到 ' + str(max_x) + ' 猜一個數字:'))
我們在更新 max_x 時,要求 guess 必須大於 max_x,否則我們便不會更新 max_x(例如本來的 max_x 是 100,guess 是 120,我們的 max_x 會保持在 100)。min_x 也是類似的邏輯。
這樣,我們便能確保用家不會在輸入錯誤的資料時,遊戲變得更加不利。
異常處理:總結
# 匯入 library import random # 定義常數(constant)及函數(variable) min_x = 0 max_x = 100 answer = random.randint(min_x,max_x) # 定義循環(loop) while True: # 先檢查 guess 是否正整數,如果不是,要求用家重新輸入正整數,直到 guess 是一個正整數 while True: guess = input('請從 ' + str(min_x) + ' 到 ' + str(max_x) + ' 猜一個數字:') if guess.isnumeric(): guess = int(guess) break # 比較用家提供的數字及範圍與答案 if guess == answer: print('恭喜您猜對了!答案是 ' + str(answer)) break # 限制 min_x,max_x 和 guess 的關係 elif (guess < answer) and (min_x < guess): min_x = guess elif (guess > answer) and (max_x > guess): max_x = guess
經過以上的修改後,我們結合了異常處理的方法。我們的編程現在比 MVP 長了些,但我們已經能夠處理很大部分的用戶輸入問題。
教學完整代碼
最後送給大家這篇教學的 Google Colab 完整代碼。如果您不懂得使用免安裝又好用的 Google Colab Notebook,記得閱讀這篇教學了:新手 1/3:5 分鐘免安裝學習 Python?Google Colab Notebook 幫緊您!
總結
在這編教學裡,我們介紹了如何從構思問題開始,一步步去編寫 Python 程式,達至 MVP 的效果。我們亦延伸到異常處理,令我們的程式能夠變得更可靠,不用擔心用戶的輸入不對。
下一篇,我們會介紹「改良程式:簡化架構」的步驟,作為一個小編程企劃裡的最後一步。敬請留意!