目錄
    Add a header to begin generating the table of contents

    實例1(下):猜數字遊戲!Colab 例子談改良程式和簡化架構

    實例1(下):猜數字遊戲!Colab 例子談改良程式和簡化架構
    Share on facebook
    Share on twitter
    Share on linkedin
    Share on whatsapp
    目錄
      Add a header to begin generating the table of contents

      上回我們完成了分析問題、實質編程和處理異常的環節,總算有一個可以面世使用的程式。

      要完成一個簡單的編程方案,最後的一步往往是簡化架構,亦即是 code refractoring。

      這一步比較進階,因為每個人對「簡化」的定義不同,在不同的企劃裡也有不同的需求。如果您是新手的話,不用著急,因為隨著您的編程經驗增加,會更加熟能生巧。

      這裡也沒有一個既定的答案,因為不同的簡化各有利弊。譬如您可能減少了一些不必要的功能,但不覺地減低了可讀性(readability)。所以,簡化架構也是需要取捨平衡的一門學問。

      我們的底線(bottomline)是令程式編程變得更容易管理和更新。

      我是廣告 ^o^

      這裡我們提供 3 個思考的方向:

      1. 使用功能(function)包裝代碼
      2. 減低可變性(mutability)
      3. 使用物件導向概念(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)。

      我是廣告 ^o^

      這樣,當我們在其他地方呼叫 loop() 的時候,我們便可以直接進入到一個循環裡。這對於編程排錯(debugging)有幫助。

      我們在另一篇教學有討論猜數字遊戲裡使用 global 的用意,有興趣可以參考這裡:局部 local 與總體 global 函數有何不同?教您使用 global keyword

      減低可變性(mutability)

      這個比較有爭議,在於這樣會把編程弄得稍微複雜,但也不乏潛在的好處。

      可變性(mutability)指的是我們能否在已定義函數後更改其值,而不用建立新的物件(object)。在 Python 裡,可變(mutable)的函數像是列表(list),而不可變(immutable)的函數像是元組(tuple)。

      減低可變性即是減低使用可變(mutable)函數。其中一個體現就是避免使用 global 關鍵字。潛在的好處是:

      我是廣告 ^o^
      1. 避免在不必要的自訂功能裡錯誤更改 global 函數。許多時我們的 global 函數其實只在某個功能裡使用,但如果我們習慣使用 global 函數,可能會意外地改變了函數值
      2. 比較容易把邏輯轉換到其他只使用不可變(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_xmax_x)。而我們本來在 loop() 所更新的 min_xmax_x 變成了 game() 的回傳值(return)。

      留意在 main() 這個功能裡,我們先定義一個「起始值」game_0。這個 game_0 呼叫第一個循環(iteration),而回傳一個元組,含有 min_xmax_xanswer 3 個函數。這個元組是不可變(immutable)的。

      我們接著把 game_0 這個元組作為函數輸入(argument)再次放進 game() 這個功能,進行第二、三、四等的循環。每個循環結束後,我們再把循環的回傳放進 game() ,直到用戶猜對數字。

      這裡我們避免了使用可變(mutable)的 global 函數處理 min_xmax_x 的更新。我們改為使用不可變的元組傳遞 min_xmax_x 的更新。

      我是廣告 ^o^

      某程度上,這個方法其實類似於遞迴(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_xmax_x。我們把他們在物件裡儲存為 self.min_xself.max_x

      這 2 個函數是一個物件的個體函數(instance variable),既可以操作物件時更改(代替 global 的功能),亦可以避免在其他地方不小心更改了他們的數值。

      我們亦定義了 Game.play() 的功能,以類似於開首使用 global 函數的方法處理 min_xmax_x 的更新。

      我是廣告 ^o^

      定義了 Game 這個物件也有很多好處,包括我們可以同時建立多個 Game 的物件,而他們之間不會影響對方。這令 Python 能夠同時處理(multithreading)多個用戶使用 Game 來玩猜數字遊戲。

      我們也可以定義一個物件函數(class variable)儲存每個用戶使用 Game 時的結果,弄成一個龍虎榜。

      教學完整代碼

      最後送給大家這篇教學的 Google Colab 完整代碼。如果您不懂得使用免安裝又好用的 Google Colab Notebook,記得閱讀這篇教學了:新手 1/3:5 分鐘免安裝學習 Python?Google Colab Notebook 幫緊您!

      總結

      經過上一篇教學,我們完成了編程設計和異常處理。這一篇教學把我們的焦點放在改良編程裡,讓我們可以把程式編程變得更容易管理和更新。

      我是廣告 ^o^

      人氣文章

      快讓我學更多

      small_c_popup.png
      想學習 Python 嗎?
      快來訂閱我們的電子報!