Python 入門指南 5.0
單元 28 - 重構
重構 (refactoring) 是指不改變軟體 (software) 外部行為下,對程式原始碼 (source code) 進行整理、修改,不外是希望提升原始碼的可讀性及更易於維護
Readability
Maintainability
Extensibility
簡單說,只要感覺原始碼有些不對勁就該重構,尤其在一個大的開發團隊中,每個小組負責的部份可能都不同,因而常常會發生識別字 (identifier) 命名不一致或是太多類別 (class) 有共通特性的情況。所以需要適時的重構原始程式碼,這也是軟體開發過程中一項重要的工作。
常見的重構技術如下
- 對屬性 (attribute) 進行封裝 (encapsulation) ,並將屬性及方法 (method) 名稱更改為具有一致性。
- 移動屬性或方法,讓其出現在更適合的位置。
- 使型態 (type) 可以更具通用性,或挑出共通的屬性、方法成父類別 (superclass) 及子類別 (subclass) 。
有許多專書討論重構技術,由於本書篇幅有限,不可能塞太多東西,所以這裡僅是初步介紹而已。
重構通常需要搭配單元測試 (unit testing) ,確保軟體各部份的功能沒有改變。
我們的 Encrypt 類別的 __init__() 方法其實有點問題
7 9 | def __init__(self):
self.set_code()
|
這樣的做法是建立 Encrypt 物件 (object) ,同時建立隨機的密碼表,雖說目前 Encrypt 類別符合我們開發的需求,倒是當我們想要擴充功能的時候就會發生問題。怎麼說呢?例如有次編碼結果不錯,我們想要下次繼續沿用相同的密碼表,這時候就需要把密碼表儲存起來了,存檔有兩種選擇,一種是儲存整個物件,另一種則是儲存密碼表的字串就好。
儲存整個物件比較麻煩,相對儲存密碼表字串 (string) 比較簡單,原因不外是字串太常用了,所以 Python 程式庫 (library) 就提供了簡單的方式來儲存字串。話說感覺到 encrypt07.py 中 Encrypt 類別的問題出在哪裡的嗎?是的,如果我們把密碼表字串儲存到某一的純文字檔案中,重新讀取回來就得設定 code 屬性,可是我們目前的版本無法替我們做到這一點。
還有就是我們利用數學公式設計一種演算法 (algorithm) ,利用這種演算法來建立密碼表,然而這個演算法並不是最理想的方式,因為這個公式建立的密碼表只有
5 × 10 = 50
依據 a 可能有 5 種數字, b 可能有 10 種數字,因此這個演算法最多就只能產生 50 種密碼表。
可是 26 個英文字母的排列會有
1 × 2 × 3 × ⋯⋯ × 25 × 26 = 4.0329 × 1026
由上可見有非常非常多種。
有沒有更理想的方式呢?有的,事實上我們可以直接攪亂字串中字母的順序,這需要另一個攪亂順序的演算法,這在標準程式庫 (standard library) 中有,另外編碼 toEncode() 跟 toDecode() 兩個方法都按照 Unicode 原始數值來做計算也太不 Python 啦! Python 是以簡潔、豐富程式庫而著稱的程式語言 (programming language) ,我們連續用了 chr() 、 ord() 、 str() 、 len() 等,雖說是內建函數 (function) 沒錯,可是卻是拿 Unicode 編碼來思考轉換,一點也沒用 Python 思考說。
下面我們直接介紹 Encrypt 類別的最終版本,並且說明重構的原因。
完整程式請參考「範例程式篇」的 encrypt.py 。
首先刪去 setcode() ,替 __init__() 新增一個參數 (parameter) new_code ,同時把 __code 屬性改為串列 (list) ,另外新增一個串列屬性 __alph
7 9 10 11 12 13 16 | def __init__(self, new_code=None):
if new_code == None:
self.__code = [chr(i) for i in range(97, 123)]
shuffle(self.__code)
else:
self.__code = list(new_code)
self.__alph = [chr(i) for i in range(97, 123)]
|
__code 屬性改成串列的原因很簡單,因為串列是 Python 的工作馬,幾乎程式裡大大小小所有的工作都交給串列比較方面,另外串列是可變的,這給了我們很大的方便,留意這一行
10 | self.__code = [chr(i) for i in range(97, 123)]
|
這是串列的綜合運算 (comprehension) ,直接在中括弧中寫運算式建立串列元素值,由於 range() 回傳參數指定範圍的整數序列,因此 i 在 for 迴圈取得的第一個值為 97 , chr(i) 回傳單一字元的字串 "a" ,因此 code 會得到 26 個英文小寫字母的串列。
下面一行
11 | shuffle(self.__code)
|
shuffle() 直接攪亂串列裡元素的順序,同樣在標準程式庫的 random 中,所以要先 import 進來
2 | from random import shuffle
|
參數 new_code 用來直接設定 code 屬性,注意這裡用內建函數 list() 將 new_code 轉換成串列,至於另一個屬性 __alph 則是英文小寫字母表,這在重構 toEncode() 及 toDecode() 的地方會用到。
然後我們也新增 __str__() 來代替原本的 code 屬性
19 20 21 | def __str__(self):
code = "".join(self.__code)
return "code: " + code
|
類別預設的 __str__() 為物件的字串形式,留意這裡是用空字串加上 join() 方法連結 code 中的所有元素。
之所以把 code 屬性刪除,主要原因是重構後內部處理的 __code 為串列,之後到 GUI 或瀏覽器都只需要密碼表字串,雖然說也可以將 code 屬性設定為密碼表字串,但是這樣就跟內建的 __str__() 在功能上重複了。
toEncode() 重構版本的編碼迴圈 (loop) 如下
29 32 33 34 35 36 | for i in original_string:
if i in self.__code:
j = self.__alph.index(i)
result += self.__code[j]
else:
result += i
|
之前的版本是利用計算編碼值,重構後的版本則是用串列的 index() 方法找索引值,也就是簡單用兩個串列的對應索引值來作替換,這點同樣用在解碼的迴圈中
47 50 51 52 53 54 | for i in original_string:
if i in self.__code:
j = self.__code.index(i)
result += self.__alph[j]
else:
result += i
|
兩個迴圈幾乎一樣,除了把 self.__alph 換成 self.__code 以外,至於原本的巢狀迴圈 (nested loop) 也不需要了。
重新仔細看一下 encrypt.py 是不是更容易理解了呢?接下來我們就要進入 GUI 的部份了,在此之前先來認識一下標準程式庫中的 Tk 吧!
中英文術語對照 | |
---|---|
演算法 | algorithm |
屬性 | attribute |
類別 | class |
綜合運算 | comprehension |
封裝 | encapsulation |
函數 | function |
識別字 | identifier |
程式庫 | library |
串列 | list |
迴圈 | loop |
方法 | method |
巢狀迴圈 | nested loop |
物件 | object |
參數 | parameter |
程式語言 | programming language |
重構 | refactoring |
軟體 | software |
原始碼 | source code |
標準程式庫 | standard library |
字串 | string |
子類別 | subclass |
父類別 | superclass |
型態 | type |
單元測試 | unit testing |
重點整理 |
---|
1. 重構是指重新整理程式碼,讓程式更易於維護。 |
2. 常見的重構技術包括整理屬性、方法,挑出類別的共通特性定義父類別等等。 |
3. Encrypt 類別的重構包括刪去 setcode() ,重新定義 __init__() ,增加 __str__() ,修改 toEncode() 與 toDecode() 等。 |
問題與討論 |
---|
1. 什麼是重構?為什麼要替開發好的程式進行重構? |
2. Encrppt 類別進行了哪些重構?為什麼要作這些重構? |
練習 |
---|
1. 將單元 22 的練習 10 重構到程式 exercise2801.py 中。 參考程式碼 |
2. 將單元 23 的練習 9 重構到程式 exercise2802.py 中。 參考程式碼 |
3. 將單元 23 的練習 10 重構到程式 exercise2803.py 中。 參考程式碼 |
4. 將單元 25 的練習 3 重構到程式 exercise2804.py 中。 參考程式碼 |
5. 將單元 25 的練習 4 重構到程式 exercise2805.py 中。 參考程式碼 |
6. 將單元 25 的練習 6 重構到程式 exercise2806.py 中。 參考程式碼 |
7. 將單元 26 的練習 4 重構到程式 exercise2807.py 中。 參考程式碼 |
8. 將單元 26 的練習 5 重構到程式 exercise2808.py 中。 參考程式碼 |
9. 將單元 26 的練習 8 重構到程式 exercise2809.py 中。 參考程式碼 |
10. 將單元 27 的練習 9 重構到程式 exercise2810.py 中。 參考程式碼 |
11. 將單元 27 的練習 10 重構到程式 exercise2811.py 中。 參考程式碼 |