Python程式設計師必知必會的開發者工具

jobbole發表於2014-02-09

  Python已經演化出了一個廣泛的生態系統,該生態系統能夠讓Python程式設計師的生活變得更加簡單,減少他們重複造輪的工作。同樣的理念也適用於工具開發者的工作,即便他們開發出的工具並沒有出現在最終的程式中。本文將介紹Python程式設計師必知必會的開發者工具。

  對於開發者來說,最實用的幫助莫過於幫助他們編寫程式碼文件了。pydoc模組可以根據原始碼中的docstrings為任何可匯入模組生成格式良好的文件。Python包含了兩個測試框架來自動測試程式碼以及驗證程式碼的正確性:1)doctest模組,該模組可以從原始碼或獨立檔案的例子中抽取出測試用例。2)unittest模組,該模組是一個全功能的自動化測試框架,該框架提供了對測試準備(test fixtures), 預定義測試集(predefined test suite)以及測試發現(test discovery)的支援。

  trace模組可以監控Python執行程式的方式,同時生成一個報表來顯示程式的每一行執行的次數。這些資訊可以用來發現未被自動化測試集所覆蓋的程式執行路徑,也可以用來研究程式呼叫圖,進而發現模組之間的依賴關係。編寫並執行測試可以發現絕大多數程式中的問題,Python使得debug工作變得更加簡單,這是因為在大部分情況下,Python都能夠將未被處理的錯誤列印到控制檯中,我們稱這些錯誤資訊為traceback。如果程式不是在文字控制檯中執行的,traceback也能夠將錯誤資訊輸出到日誌檔案或是訊息對話方塊中。當標準的traceback無法提供足夠的資訊時,可以使用cgitb 模組來檢視各級棧和原始碼上下文中的詳細資訊,比如區域性變數。cgitb模組還能夠將這些跟蹤資訊以HTML的形式輸出,用來報告web應用中的錯誤。

  一旦發現了問題出在哪裡後,就需要使用到互動式偵錯程式進入到程式碼中進行除錯工作了,pdb模組能夠很好地勝任這項工作。該模組可以顯示出程式在錯誤產生時的執行路徑,同時可以動態地調整物件和程式碼進行除錯。當程式通過測試並除錯後,下一步就是要將注意力放到效能上了。開發者可以使用profile以及timit模組來測試程式的速度,找出程式中到底是哪裡很慢,進而對這部分程式碼獨立出來進行調優的工作。Python程式是通過直譯器執行的,直譯器的輸入是原有程式的位元組碼編譯版本。這個位元組碼編譯版本可以在程式執行時動態地生成,也可以在程式打包的時候就生成。compileall模組可以處理程式打包的事宜,它暴露出了打包相關的介面,該介面能夠被安裝程式和打包工具用來生成包含模組位元組碼的檔案。同時,在開發環境中,compileall模組也可以用來驗證原始檔是否包含了語法錯誤。

  在原始碼級別,pyclbr模組提供了一個類檢視器,方便文字編輯器或是其他程式對Python程式中有意思的字元進行掃描,比如函式或者是類。在提供了類檢視器以後,就無需引入程式碼,這樣就避免了潛在的副作用影響。

 文件字串與doctest模組

  如果函式,類或者是模組的第一行是一個字串,那麼這個字串就是一個文件字串。可以認為包含文件字串是一個良好的程式設計習慣,這是因為這些字串可以給Python程式開發工具提供一些資訊。比如,help()命令能夠檢測文件字串,Python相關的IDE也能夠進行檢測文件字串的工作。由於程式設計師傾向於在互動式shell中檢視文件字串,所以最好將這些字串寫的簡短一些。例如

# mult.py
class Test:
    """
    >>> a=Test(5)
    >>> a.multiply_by_2()
    10
    """
    def __init__(self, number):
        self._number=number

    def multiply_by_2(self):
        return self._number*2

  在編寫文件時,一個常見的問題就是如何保持文件和實際程式碼的同步。例如,程式設計師也許會修改函式的實現,但是卻忘記了更新文件。針對這個問題,我們可以使用doctest模組。doctest模組收集文件字串,並對它們進行掃描,然後將它們作為測試進行執行。為了使用doctest模組,我們通常會新建一個用於測試的獨立的模組。例如,如果前面的例子Test class包含在檔案mult.py中,那麼,你應該新建一個testmult.py檔案用來測試,如下所示:

# testmult.py

import mult, doctest

doctest.testmod(mult, verbose=True)

# Trying:
#     a=Test(5)
# Expecting nothing
# ok
# Trying:
#     a.multiply_by_2()
# Expecting:
#     10
# ok
# 3 items had no tests:
#     mult
#     mult.Test.__init__
#     mult.Test.multiply_by_2
# 1 items passed all tests:
#    2 tests in mult.Test
# 2 tests in 4 items.
# 2 passed and 0 failed.
# Test passed.

  在這段程式碼中,doctest.testmod(module)會執行特定模組的測試,並且返回測試失敗的個數以及測試的總數目。如果所有的測試都通過了,那麼不會產生任何輸出。否則的話,你將會看到一個失敗報告,用來顯示期望值和實際值之間的差別。如果你想看到測試的詳細輸出,你可以使用testmod(module, verbose=True).

  如果不想新建一個單獨的測試檔案的話,那麼另一種選擇就是在檔案末尾包含相應的測試程式碼:

if __name__ == '__main__':
    import doctest
    doctest.testmod()

  如果想執行這類測試的話,我們可以通過-m選項呼叫doctest模組。通常來講,當執行測試的時候沒有任何的輸出。如果想檢視詳細資訊的話,可以加上-v選項。

$ python -m doctest -v mult.py

 單元測試與unittest模組

  如果想更加徹底地對程式進行測試,我們可以使用unittest模組。通過單元測試,開發者可以為構成程式的每一個元素(例如,獨立的函式,方法,類以及模組)編寫一系列獨立的測試用例。當測試更大的程式時,這些測試就可以作為基石來驗證程式的正確性。當我們的程式變得越來越大的時候,對不同構件的單元測試就可以組合起來成為更大的測試框架以及測試工具。這能夠極大地簡化軟體測試的工作,為找到並解決軟體問題提供了便利。

# splitter.py
import unittest

def split(line, types=None, delimiter=None):
    """Splits a line of text and optionally performs type conversion.
    ...
    """
    fields = line.split(delimiter)
    if types:
        fields = [ ty(val) for ty,val in zip(types,fields) ]
    return fields

class TestSplitFunction(unittest.TestCase):
    def setUp(self):
        # Perform set up actions (if any)
        pass
    def tearDown(self):
        # Perform clean-up actions (if any)
        pass
    def testsimplestring(self):
        r = split('GOOG 100 490.50')
        self.assertEqual(r,['GOOG','100','490.50'])
    def testtypeconvert(self):
        r = split('GOOG 100 490.50',[str, int, float])
        self.assertEqual(r,['GOOG', 100, 490.5])
    def testdelimiter(self):
        r = split('GOOG,100,490.50',delimiter=',')
        self.assertEqual(r,['GOOG','100','490.50'])

# Run the unittests
if __name__ == '__main__':
    unittest.main()

#...
#----------------------------------------------------------------------
#Ran 3 tests in 0.001s

#OK

  在使用單元測試時,我們需要定義一個繼承自unittest.TestCase的類。在這個類裡面,每一個測試都以方法的形式進行定義,並都以test打頭進行命名——例如,’testsimplestring‘,’testtypeconvert‘以及類似的命名方式(有必要強調一下,只要方法名以test打頭,那麼無論怎麼命名都是可以的)。在每個測試中,斷言可以用來對不同的條件進行檢查。

  實際的例子:

  假如你在程式裡有一個方法,這個方法的輸出指向標準輸出(sys.stdout)。這通常意味著是往螢幕上輸出文字資訊。如果你想對你的程式碼進行測試來證明這一點,只要給出相應的輸入,那麼對應的輸出就會被顯示出來。

# url.py

def urlprint(protocol, host, domain):
    url = '{}://{}.{}'.format(protocol, host, domain)
    print(url)

  內建的print函式在預設情況下會往sys.stdout傳送輸出。為了測試輸出已經實際到達,你可以使用一個替身物件對其進行模擬,並且對程式的期望值進行斷言。unittest.mock模組中的patch()方法可以只在執行測試的上下文中才替換物件,在測試完成後就立刻返回物件原始的狀態。下面是urlprint()方法的測試程式碼:

#urltest.py

from io import StringIO
from unittest import TestCase
from unittest.mock import patch
import url

class TestURLPrint(TestCase):
    def test_url_gets_to_stdout(self):
        protocol = 'http'
        host = 'www'
        domain = 'example.com'
        expected_url = '{}://{}.{}\n'.format(protocol, host, domain)

        with patch('sys.stdout', new=StringIO()) as fake_out:
            url.urlprint(protocol, host, domain)
            self.assertEqual(fake_out.getvalue(), expected_url)

  urlprint()函式有三個引數,測試程式碼首先給每個引數賦了一個假值。變數expected_url包含了期望的輸出字串。為了能夠執行測試,我們使用了unittest.mock.patch()方法作為上下文管理器,把標準輸出sys.stdout替換為了StringIO物件,這樣傳送的標準輸出的內容就會被StringIO物件所接收。變數fake_out就是在這一過程中所建立出的模擬物件,該物件能夠在with所處的程式碼塊中所使用,來進行一系列的測試檢查。當with語句完成時,patch方法能夠將所有的東西都復原到測試執行之前的狀態,就好像測試沒有執行一樣,而這無需任何額外的工作。但對於某些Python的C擴充套件來講,這個例子卻顯得毫無意義,這是因為這些C擴充套件程式繞過了sys.stdout的設定,直接將輸出傳送到了標準輸出上。這個例子僅適用於純Python程式碼的程式(如果你想捕獲到類似C擴充套件的輸入輸出,那麼你可以通過開啟一個臨時檔案然後將標準輸出重定向到該檔案的技巧來進行實現)。

相關文章