上回我們完成了分析問題、實質編程和處理異常的環節,總算有一個可以面世使用的程式。
要完成一個簡單的編程方案,最後的一步往往是簡化架構,亦即是 code refractoring。
這一步比較進階,因為每個人對「簡化」的定義不同,在不同的企劃裡也有不同的需求。如果您是新手的話,不用著急,因為隨著您的編程經驗增加,會更加熟能生巧。
這裡也沒有一個既定的答案,因為不同的簡化各有利弊。譬如您可能減少了一些不必要的功能,但不覺地減低了可讀性(readability)。所以,簡化架構也是需要取捨平衡的一門學問。
我們的底線(bottomline)是令程式編程變得更容易管理和更新。
這裡我們提供 3 個思考的方向:
- 使用功能(function)包裝代碼
- 減低可變性(mutability)
- 使用物件導向概念(object oriented)
使用功能(function)包裝代碼
第一個可以讓我們改良架構的方法是使用自訂功能(custom function)包裝代碼。
這是因為如果我們的代碼是一個 function 的形式,那麼我們在其他地方使用這個程式時,只需要輸入 customFunction()
便可以運行代碼。
另一方面,我們亦應該把重複使用的代碼濃縮成自訂功能,讓我們更改代碼時,不用東找西找需要更改的地方。
# 匯入 library import random from sys import exit # 定義常數(constant)及函數(variable) min_x = 1 max_x = 100 answer = random.randint(min_x,max_x) def loop(): global min_x, max_x # 定義循環(loop) while True: # 先檢查 guess 是否正整數,如果不是,要求用家重新輸入正整數,直到 guess 是一個正整數 guess = input('請從 ' + str(min_x) + ' 到 ' + str(max_x) + ' 猜一個數字:') if guess.isnumeric(): guess = int(guess) break # 比較用家提供的數字及範圍與答案 if guess == answer: exit('恭喜您猜對了!答案是 ' + 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 def main(): while True: loop() if __name__ == '__main__': main()
我們在這個版本裡,把每一個循環(iteration)所需的代碼定義在 loop()
這個自訂功能裡。而我們透過 main()
這一個自訂功能處理循環(loop)。
這樣,當我們在其他地方呼叫 loop() 的時候,我們便可以直接進入到一個循環裡。這對於編程排錯(debugging)有幫助。
我們在另一篇教學有討論猜數字遊戲裡使用 global
的用意,有興趣可以參考這裡:局部 local 與總體 global 函數有何不同?教您使用 global keyword
減低可變性(mutability)
這個比較有爭議,在於這樣會把編程弄得稍微複雜,但也不乏潛在的好處。
可變性(mutability)指的是我們能否在已定義函數後更改其值,而不用建立新的物件(object)。在 Python 裡,可變(mutable)的函數像是列表(list),而不可變(immutable)的函數像是元組(tuple)。
減低可變性即是減低使用可變(mutable)函數。其中一個體現就是避免使用 global
關鍵字。潛在的好處是:
- 避免在不必要的自訂功能裡錯誤更改 global 函數。許多時我們的 global 函數其實只在某個功能裡使用,但如果我們習慣使用 global 函數,可能會意外地改變了函數值
- 比較容易把邏輯轉換到其他只使用不可變(immutable)函數的編程語言
# 匯入 library import random from sys import exit def game(min_x, max_x, answer): while True: # 先檢查 guess 是否正整數,如果不是,要求用家重新輸入正整數,直到 guess 是一個正整數 guess = input('請從 ' + str(min_x) + ' 到 ' + str(max_x) + ' 猜一個數字:') if guess.isnumeric(): guess = int(guess) break # 比較用家提供的數字及範圍與答案 if guess == answer: exit('恭喜您猜對了!答案是 ' + str(answer)) # 限制 min_x,max_x 和 guess 的關係 elif (guess < answer) and (min_x < guess): return guess, max_x, answer elif (guess > answer) and (max_x > guess): return min_x, guess, answer else: return min_x, max_x, answer def main(): game_0 = game(1, 100, random.randint(1, 100)) while True: game_0 = game(*game_0) if __name__ == '__main__': main()
在以上的例子裡,我們把 loop()
改為 game()
這個功能。而重點是,loop()
沒有函數輸入(input argument),但 game()
卻有 3 個函數輸入。
game()
所要求的函數其實就是我們本來的 global
函數(例如 min_x
和 max_x
)。而我們本來在 loop()
所更新的 min_x
和 max_x
變成了 game()
的回傳值(return)。
留意在 main()
這個功能裡,我們先定義一個「起始值」game_0
。這個 game_0
呼叫第一個循環(iteration),而回傳一個元組,含有 min_x
、max_x
和 answer
3 個函數。這個元組是不可變(immutable)的。
我們接著把 game_0
這個元組作為函數輸入(argument)再次放進 game()
這個功能,進行第二、三、四等的循環。每個循環結束後,我們再把循環的回傳放進 game()
,直到用戶猜對數字。
這裡我們避免了使用可變(mutable)的 global
函數處理 min_x
和 max_x
的更新。我們改為使用不可變的元組傳遞 min_x
和 max_x
的更新。
某程度上,這個方法其實類似於遞迴(recursion)。雖然記憶體的使用可能比較多,但是也不乏好處。
使用物件導向概念(object oriented)
最後一個方法就是使用物件導向的概念(object oriented principles)。這個是一個普遍備受認證的概念,我們一起試試把這個猜數字遊戲變成物件導向?
import random class Game: def __init__(self, min_x, max_x): self.min_x = min_x self.max_x = max_x def play(self): self.answer = random.randint(self.min_x, self.max_x) while True: while True: # 先檢查 guess 是否正整數,如果不是,要求用家重新輸入正整數,直到 guess 是一個正整數 guess = input('請從 ' + str(self.min_x) + ' 到 ' + str(self.max_x) + ' 猜一個數字:') if guess.isnumeric(): guess = int(guess) break # 比較用家提供的數字及範圍與答案 if guess == self.answer: print('恭喜您猜對了!答案是 ' + str(self.answer)) break # 限制 min_x,max_x 和 guess 的關係 elif (guess < self.answer) and (self.min_x < guess): self.min_x = guess elif (guess > self.answer) and (self.max_x > guess): self.max_x = guess def main(): game = Game(1,100) game.play() if __name__ == '__main__': main()
以上我們定義了 Game 作為一個物件(object)。留意我們要求建立新的 Game 物件時,需要提供 2 個函數:min_x
和 max_x
。我們把他們在物件裡儲存為 self.min_x
和 self.max_x
。
這 2 個函數是一個物件的個體函數(instance variable),既可以操作物件時更改(代替 global 的功能),亦可以避免在其他地方不小心更改了他們的數值。
我們亦定義了 Game.play()
的功能,以類似於開首使用 global
函數的方法處理 min_x
和 max_x
的更新。
定義了 Game 這個物件也有很多好處,包括我們可以同時建立多個 Game 的物件,而他們之間不會影響對方。這令 Python 能夠同時處理(multithreading)多個用戶使用 Game 來玩猜數字遊戲。
我們也可以定義一個物件函數(class variable)儲存每個用戶使用 Game 時的結果,弄成一個龍虎榜。
教學完整代碼
最後送給大家這篇教學的 Google Colab 完整代碼。如果您不懂得使用免安裝又好用的 Google Colab Notebook,記得閱讀這篇教學了:新手 1/3:5 分鐘免安裝學習 Python?Google Colab Notebook 幫緊您!
總結
經過上一篇教學,我們完成了編程設計和異常處理。這一篇教學把我們的焦點放在改良編程裡,讓我們可以把程式編程變得更容易管理和更新。