程式扎記: Python Tutorial 第五堂(3)使用 assert 與 doctest

標籤

2015年2月15日 星期日

Python Tutorial 第五堂(3)使用 assert 與 doctest

Source From Here
Preface
對於靜態定型語言(Statically-typing language),因為變數有型態資訊,因而編譯器等工具,可以在程式運行之前檢查出許多型態不正確的資訊

Python 是動態定型語言Dynamically-typing language),也就是說,在 Python 中變數沒有型態,只是用來作為參考實際物件的一根柄Handle,如果有型態錯誤上的操作,基本上會是在執行時期運行至該段程式碼時,才會產生錯誤訊息,因此對於 Python 來說,檢查出型態不正確的任務,必須開發者本身來承擔,為程式設計測試程式,會是個不錯的方式之一

對於靜態定型語言,雖然有編譯器等工具,協助開發者於程式運行之前檢查型態錯誤問題,然而,設計優良測試程式檢測執行時期功能是否符合預期亦非常重要;對於動態語言,現在也有一些型態註解方案,可提供分析工具於程式運行前檢查型態資訊,像是 Python 的 PEP-3170 提出的 Function annotation。

在 Python 的世界中,當然不乏撰寫測試的相關工具,像是 :
* assert 陳述 在程式中安插除錯用的斷言(Assertion)檢查時很方便的一個方式。
* doctest 模組 在程式碼中找尋類似 Python 互動環境的文字片段,執行並驗證程式是否如預期方式執行。
* unittest 模組 有時稱為 “PyUnit",是 JUnit 的 Python 語言實現。
* 第三方測試工具: nosepytest

每個模組中都會有的 __name__ 全域變數 (The function’s name. Check Data Model),當你執行直接某個 Python 模組時,例如:
# python fibo.py

當模組使用時的程式碼會像 import 時般運行,不過如果當作一般 .py 檔執行時, __name__ 這個變數會被設定為 '__main__' 這個字串名稱,因此,如果想要為這個模組撰寫一個簡單的自我測試,可以如以下方式撰寫:
  1. if __name__ == "__main__":  
  2.     #測試的程式碼  
當你直接執行某個模組時,if 條件才會成立,測試的程式碼才會執行,而 import 該模組時,因為 __name__ 會是模組名稱,因此就不會在 import 執行測試的程式碼。

Assertions in Python
要在程式中安插斷言,使用 assert 很方便,其語法如下:
  1. assert_stmt ::=  "assert" expression ["," reason]  
使用 assert expression 的話,相當於以下的程式片段:
  1. if __debug__:  
  2.     if not expression: raise AssertionError  
如果包括第二個 reason statement,例如 assert expressionreason,相當於以下的程式片段:
  1. if __debug__:  
  2.     if not expression: raise AssertionError(reason)  
也就是說,reason 會被當作 AssertionError 的錯誤資訊結果。

__debug__ 是個內建變數,一般情況下會是 True,如果執行時需要最佳化時(在執行時加上 -O 引數)則會是 False。例如以下是互動環境中的一些例子:


那麼何時該使用斷言呢?…一般有幾個建議:
* 前置條件通常在私有函式之中)斷言客戶端呼叫函式前,已經準備好某些條件。
* 後置條件 驗證客戶端呼叫函式後,具有函式承諾有結果。
* 類別不變量Class invariant)驗證物件某個時間點下的狀態。
* 內部不變量Internal invariant)使用斷言取代註解。
* 流程不變量Control-flow invariant)斷言程式流程中絕不會執行到的程式碼部份。

前置條件斷言的例子如下:
  1. def __set_refresh_Interval(interval):  
  2.     if interval > 0 and interval <= 1000 / MAX_REFRESH_RATE:  
  3.         raise ValueError('Illegal interval: ' + interval)  
  4.     # 函式中的程式流程  
程式中的 if 檢查進行了 防禦式程式設計Defensive programming),如果想要用 assert 取代,可以如下:
  1. def __set_refresh_Interval(rate):  
  2.     (assert interval > 0 and interval <= 1000 / MAX_REFRESH_RATE,   
  3.             'Illegal interval: ' + interval)  
  4.     # 函式中的程式流程  
防禦式程式設計有些不好的名聲,不過並不是做了防禦式程式設計就不好,可以參考 避免隱藏錯誤的防禦性設計

一個內部不變量的例子則是如下:
  1. if balance >= 10000:  
  2.     ...  
  3. elif 10000 > balance >= 100:  
  4.     ...  
  5. else: # balance 一定是少於 100 的情況  
  6.     ...  
如果要在 else 的 balance 少於 100 的情況下拋出 AssertionError,以實現速錯(Fail fast)概念,而不是只使用註解來提醒開發者,則可以改為以下:
  1. if balance >= 10000:  
  2.     ...  
  3. else if 10000 > balance >= 100:  
  4.     ...  
  5. else:  
  6.     assert balance < 100, balance  
  7.     ...  
另一個情況是:
  1. if suit == Suit.CLUBS:  
  2.     ...  
  3. elif suit == Suit.DIAMONDS:  
  4.     ...  
  5. elif suit == Suit.HEARTS:  
  6.     ...  
  7. elif suit == Suit.SPADES:  
  8.     ...  
如果列舉檢查只會有以上四個條件,也可以運用斷言來實現速錯:
  1. if suit == Suit.CLUBS:  
  2.     ...  
  3. elif suit == Suit.DIAMONDS:  
  4.     ...  
  5. elif suit == Suit.HEARTS:  
  6.     ...  
  7. elif suit == Suit.SPADES:  
  8.     ...  
  9. else:  
  10.     assert False, suit  
程式碼中有些一定不會執行到的流程區段,可以使用斷言來確保這些區段被執行時拋出錯誤。例如:
  1. def foo(list):  
  2.     for ele in list:  
  3.         if ...:  
  4.             return  
  5.     # 這邊應該永遠不會被執行到  
可以改為:
  1. def foo(list):  
  2.     for ele in list:  
  3.         if ...:  
  4.             return  
  5.     assert False  
doctest
doctest 一方面是測試程式碼,一方面也是用來確認 docStrings 的內容沒有過期,基本上它驗證互動式的範例來執行 回歸測試(Regression testing),開發者只要為套件撰寫輸入輸出式的教學範例就可以了,這有點文學測試(Literate testing) 或可執行文件(executable documentation)的味道。

舉例來說,你也許為 util.py 中的 sorted 撰寫了以下的 docstrings:
- util.py
  1. ascending = lambda a,b: a-b  
  2. descending = lambda a,b: b-a  
  3.   
  4. def __select(xs, compare):  
  5.     xs_sorted = []  
  6.     for v in xs:  
  7.         if len(xs_sorted) == 0: xs_sorted.append(v)  
  8.         else:  
  9.             bNotFound=True  
  10.             for (i,x) in enumerate(xs_sorted):  
  11.                 if compare(x,v)>0:  
  12.                     bNotFound=False  
  13.                     xs_sorted.insert(i, v)  
  14.                     break  
  15.             if bNotFound: xs_sorted.append(v)  
  16.     return xs_sorted   
  17.   
  18. def sorted(xs, compare = ascending):  
  19.     '''  
  20.     sorted(xs) -> new sorted list from xs' item in ascending order.  
  21.     sorted(xs, func) -> new sorted list. func should return a negative integer,   
  22.                         zero, or a positive integer as the first argument is   
  23.                         less than, equal to, or greater than the second.  
  24.   
  25.     >>> sorted([21365])  
  26.     [12356]  
  27.     >>> sorted([21365], ascending)  
  28.     [12356]  
  29.     >>> sorted([21365], descending)  
  30.     [65321]  
  31.     >>> sorted([21365], lambda a, b: a - b)  
  32.     [12356]  
  33.     >>> sorted([21365], lambda a, b: b - a)  
  34.     [65321]  
  35.     '''  
  36.   
  37.     return [] if not xs else __select(xs, compare)  
  38.   
  39. if __name__ == '__main__':  
  40.     import doctest  
  41.     doctest.testmod()  
那麼直接執行模組時,就會執行測試,加上 -v 會顯示細節:


你也可以將這類文件寫在文字檔案中,例如一個 util_test.txt
  1. The ``util`` module  
  2. ======================  
  3.   
  4. Using ``sorted``  
  5. -------------------  
  6.   
  7. >>> from util import *  
  8. >>> sorted([21365])  
  9. [12356]  
  10. >>> sorted([21365], ascending)  
  11. [12356]  
  12. >>> sorted([21365], descending)  
  13. [65321]  
  14. >>> sorted([21365], lambda a, b: a - b)  
  15. [12356]  
  16. >>> sorted([21365], lambda a, b: b - a)  
  17. [65321]  
而 util.py 中改寫為以下,就可以從文字檔案中讀取內容並執行測試:
  1. if __name__ == '__main__':  
  2.     import doctest  
  3.     doctest.testfile(“util_test.txt")  
你也可以直接執行 doctest 模組 來載入測試用的文字檔案以執行測試,例如:


Supplement
Python Gossip: 使用 assert
所謂斷言(Assertion),指的是程式進行到某個時間點,斷定其必然是某種狀態,具體而言,也就是斷定該時間點上,某變數必然是某值,或某物件必具擁有何種特性值...


沒有留言:

張貼留言

網誌存檔