<dd id="sga4y"><optgroup id="sga4y"></optgroup></dd>
<nav id="sga4y"><code id="sga4y"></code></nav>
  • <optgroup id="sga4y"></optgroup>

    15.3. 重構

    全面的單元測試帶來的最大好處不是你的全部測試用例最終通過時的成就感;也不是被責怪破壞了別人的代碼時能夠證明 自己的自信。最大的好處是單元測試給了你自由去無情地重構。

    重構是在可運行代碼的基礎上使之工作得更好的過程。通常,“更好”意味著“更快”,也可能意味著 “使用更少的內存”,或者 “使用更少的磁盤空間”,或者僅僅是“更優雅的代碼”。不管對你,對你的項目意味什么,在你的環境中,重構對任何程序的長期良性運轉都是重要的。

    這里,“更好” 意味著 “更快”。更具體地說,fromRoman 函數可以更快,關鍵在于那個丑陋的、用于驗證羅馬數字有效性的正則表達式。嘗試不用正則表達式去解決是不值得的 (這樣做很難,而且可能也快不了多少),但可以通過預編譯正則表達式使函數提速。

    例 15.10. 編譯正則表達式

    >>> import re
    >>> pattern = '^M?M?M?$'
    >>> re.search(pattern, 'M')               1
    <SRE_Match object at 01090490>
    >>> compiledPattern = re.compile(pattern) 2
    >>> compiledPattern
    <SRE_Pattern object at 00F06E28>
    >>> dir(compiledPattern)                  3
    ['findall', 'match', 'scanner', 'search', 'split', 'sub', 'subn']
    >>> compiledPattern.search('M')           4
    <SRE_Match object at 01104928>
    1 這是你看到過的 re.search 語法。把一個正則表達式作為字符串 (pattern) 并用這個字符串來匹配 ('M')。如果能夠匹配,函數返回 一個 match 對象,可以用來確定匹配的部分和如何匹配的。
    2 這里是一個新的語法:re.compile 把一個正則表達式作為字符串參數接受并返回一個 pattern 對象。注意這里沒去匹配字符串。編譯正則表達式和以特定字符串 ('M') 進行匹配不是一回事,所牽扯的只是正則表達式本身。
    3 re.compile 返回的已編譯的 pattern 對象有幾個值得關注的功能:包括了幾個 re 模塊直接提供的功能 (比如:searchsub)。
    4 'M' 作參數來調用已編譯的 pattern 對象的 search 函數與用正則表達式和字符串 'M' 調用 re.search 可以得到相同的結果,只是快了很多。 (事實上,re.search 函數僅僅將正則表達式編譯,然后為你調用編譯后的 pattern 對象的 search 方法。)
    注意
    在需要多次使用同一個正則表達式的情況下,應該將它進行編譯以獲得一個 pattern 對象,然后直接調用這個 pattern 對象的方法。

    例 15.11. roman81.py 中已編譯的正則表達式

    這個文件可以在例子目錄下的 py/roman/stage8/ 目錄中找到。

    如果您還沒有下載本書附帶的樣例程序, 可以 下載本程序和其他樣例程序

    # toRoman and rest of module omitted for clarity
    
    romanNumeralPattern = \
        re.compile('^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$') 1
    
    def fromRoman(s):
        """convert Roman numeral to integer"""
        if not s:
            raise InvalidRomanNumeralError, 'Input can not be blank'
        if not romanNumeralPattern.search(s):                                    2
            raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s
    
        result = 0
        index = 0
        for numeral, integer in romanNumeralMap:
            while s[index:index+len(numeral)] == numeral:
                result += integer
                index += len(numeral)
        return result
    
    1 看起來很相似,但實質卻有很大改變。romanNumeralPattern 不再是一個字符串了,而是一個由 re.compile 返回的 pattern 對象。
    2 這意味著你可以直接調用 romanNumeralPattern 的方法。這比每次調用 re.search 要快很多。模塊被首次導入 (import) 之時,正則表達式被一次編譯并存儲于 romanNumeralPattern。之后每次調用 fromRoman 時,你可以立刻以正則表達式匹配輸入的字符串,而不需要在重復背后的這些編譯的工作。

    那么編譯正則表達式可以提速多少呢?你自己來看吧:

    例 15.12. 用 romantest81.py 測試 roman81.py 的結果

    .............          1
    ----------------------------------------------------------------------
    Ran 13 tests in 3.385s 2
    
    OK                     3
    1 有一點說明一下:這里,我在運行單元測試時沒有 使用 -v 選項,因此輸出的也不再是每個測試完整的 doc string,而是用一個圓點來表示每個通過的測試。(失敗的測試標用 F 表示,發生錯誤則用 E 表示,你仍舊可以獲得失敗和錯誤的完整追蹤信息以便查找問題所在。)
    2 運行 13 個測試耗時 3.385 秒,與之相比是沒有預編譯正則表達式時的 3.685秒。這是一個 8% 的整體提速,記住單元測試的大量時間實際上花在做其他工作上。(我單獨測試了正則表達式部分的耗時,不考慮單元測試的其他環節,正則表達式編譯可以讓匹配 search 平均提速 54%。)小小修改還真是值得。
    3 對了,不必顧慮什么,預先編譯正則表達式并沒有破壞什么,你剛剛證實這一點。

    我還想做另外一個性能優化工作。就正則表達式語法的復雜性而言,通常有不止一種方法來構造相同的表達式是不會令人驚訝的。在 comp.lang.python 上對該模塊進行一些討論后,有人建議我使用 {m,n} 語法來查找可選重復字符。

    例 15.13. roman82.py

    這個文件可以在例子目錄下的 py/roman/stage8/ 目錄中找到。

    如果您還沒有下載本書附帶的樣例程序, 可以 下載本程序和其他樣例程序

    # rest of program omitted for clarity
    
    #old version
    #romanNumeralPattern = \
    #   re.compile('^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$')
    
    #new version
    romanNumeralPattern = \
        re.compile('^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$') 1
    
    1 你已經將 M?M?M?M? 替換為 M{0,4}。它們的含義相同:“匹配 0 到 4 個 M 字符”。類似地,C?C?C? 改成了 C{0,3} (“匹配 0 到 3 個 C 字符”) 接下來的 XI 也一樣。

    這樣的正則表達簡短一些 (雖然可讀性不太好)。核心問題是,是否能加快速度?

    例 15.14. 以 romantest82.py 測試 roman82.py 的結果

    .............
    ----------------------------------------------------------------------
    Ran 13 tests in 3.315s 1
    
    OK                     2
    1 總體而言,這種正則表達使單元測試提速 2%。這不太令人振奮,但記住 search 函數只是整體單元測試的一個小部分,很多時間花在了其他方面。(我另外的測試表明這個應用了新語法的正則表達式使 search 函數提速 11% 。) 通過預先編譯和使用新語法重寫可以使正則表達式的性能提升超過 60%,令單元測試的整體性能提升超過 10%
    2 比任何的性能提升更重要的是模塊仍然運轉完好。這便是我早先提到的自由:自由地調整、修改或者重寫任何部分并且保證在此過程中沒有把事情搞得一團糟。這并不是給無休止地為了調整代碼而調整代碼以許可;你有很切實的目標 (“fromRoman 更快”),而且你可以實現這個目標,不會因為考慮在改動過程中是否會引入新的 Bug 而有所遲疑。

    還有另外一個我想做的調整,我保證這是最后一個,之后我會停下來,讓這個模塊歇歇。就像你多次看到的,正則表達式越晦澀難懂越快,我可不想在六個月內再回頭試圖維護它。是呀!測試用例通過了,我便知道它工作正常,但如果我搞不懂它是如何 工作的,添加新功能、修正新 Bug,或者維護它都將變得很困難。正如你在 第 7.5 節 “松散正則表達式” 看到的,Python 提供了逐行注釋你的邏輯的方法。

    例 15.15. roman83.py

    該文件可以在例子目錄下的 py/roman/stage8/ 目錄中找到。

    如果您還沒有下載本書附帶的樣例程序, 可以 下載本程序和其他樣例程序

    # rest of program omitted for clarity
    
    #old version
    #romanNumeralPattern = \
    #   re.compile('^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$')
    
    #new version
    romanNumeralPattern = re.compile('''
        ^                   # beginning of string
        M{0,4}              # thousands - 0 to 4 M's
        (CM|CD|D?C{0,3})    # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C's),
                            #            or 500-800 (D, followed by 0 to 3 C's)
        (XC|XL|L?X{0,3})    # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's),
                            #        or 50-80 (L, followed by 0 to 3 X's)
        (IX|IV|V?I{0,3})    # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's),
                            #        or 5-8 (V, followed by 0 to 3 I's)
        $                   # end of string
        ''', re.VERBOSE) 1
    
    1 re.compile 函數的第二個參數是可選的,這個參數通過一個或一組標志 (flag) 來控制預編譯正則表達式的選項。這里你指定了 re.VERBOSE 選項,告訴 Python 正則表達式里有內聯注釋。注釋和它們周圍的空白 會被認做正則表達式的一部分,在編譯正則表達式時 re.compile 函數會忽略它們。這個新 “verbose” 版本與老版本完全一樣,只是更具可讀性。

    例 15.16. 用 romantest83.py 測試 roman83.py 的結果

    .............
    ----------------------------------------------------------------------
    Ran 13 tests in 3.315s 1
    
    OK                     2
    1 新 “verbose” 版本和老版本的運行速度一樣。事實上,編譯的 pattern 對象也一樣,因為 re.compile 函數會剔除掉所有你添加的內容。
    2 新 “verbose” 版本可以通過所有老版本通過的測試。什么都沒有改變,但在六個月后重讀該模塊的程序員卻有了理解功能如何實現的機會。
    97av