Python 程序員必知必會的開發(fā)者工具
Python已經(jīng)演化出了一個廣泛的生態(tài)系統(tǒng),該生態(tài)系統(tǒng)能夠讓Python程序員的生活變得更加簡單,減少他們重復(fù)造輪的工作。同樣的理念也適用于工具開發(fā)者的工作,即便他們開發(fā)出的工具并沒有出現(xiàn)在最終的程序中。本文將介紹Python程序員必知必會的開發(fā)者工具。
對于開發(fā)者來說,最實用的幫助莫過于幫助他們編寫代碼文檔了。pydoc模塊可以根據(jù)源代碼中的docstrings為任何可導(dǎo)入模塊生成格式良好的文檔。Python包含了兩個測試框架來自動測試代碼以及驗證代碼的正確性:1)doctest模塊,該模塊可以從源代碼或獨(dú)立文件的例子中抽取出測試用例。2)unittest模塊,該模塊是一個全功能的自動化測試框架,該框架提供了對測試準(zhǔn)備(test fixtures), 預(yù)定義測試集(predefined test suite)以及測試發(fā)現(xiàn)(test discovery)的支持。
trace模 塊可以監(jiān)控Python執(zhí)行程序的方式,同時生成一個報表來顯示程序的每一行執(zhí)行的次數(shù)。這些信息可以用來發(fā)現(xiàn)未被自動化測試集所覆蓋的程序執(zhí)行路徑,也 可以用來研究程序調(diào)用圖,進(jìn)而發(fā)現(xiàn)模塊之間的依賴關(guān)系。編寫并執(zhí)行測試可以發(fā)現(xiàn)絕大多數(shù)程序中的問題,Python使得debug工作變得更加簡單,這是 因為在大部分情況下,Python都能夠?qū)⑽幢惶幚淼腻e誤打印到控制臺中,我們稱這些錯誤信息為traceback。如果程序不是在文本控制臺中運(yùn)行 的,traceback也能夠?qū)㈠e誤信息輸出到日志文件或是消息對話框中。當(dāng)標(biāo)準(zhǔn)的traceback無法提供足夠的信息時,可以使用cgitb 模塊來查看各級棧和源代碼上下文中的詳細(xì)信息,比如局部變量。cgitb模塊還能夠?qū)⑦@些跟蹤信息以HTML的形式輸出,用來報告web應(yīng)用中的錯誤。
一旦發(fā)現(xiàn)了問題出在哪里后,就需要使用到交互式調(diào)試器進(jìn)入到代碼中進(jìn)行調(diào)試工作了,pdb模塊能夠很好地勝任這項工作。該模塊可以顯示出程序在錯誤產(chǎn)生時的執(zhí)行路徑,同時可以動態(tài)地調(diào)整對象和代碼進(jìn)行調(diào)試。當(dāng)程序通過測試并調(diào)試后,下一步就是要將注意力放到性能上了。開發(fā)者可以使用profile以及timit模塊來測試程序的速度,找出程序中到底是哪里很慢,進(jìn)而對這部分代碼獨(dú)立出來進(jìn)行調(diào)優(yōu)的工作。Python程序是通過解釋器執(zhí)行的,解釋器的輸入是原有程序的字節(jié)碼編譯版本。這個字節(jié)碼編譯版本可以在程序執(zhí)行時動態(tài)地生成,也可以在程序打包的時候就生成。compileall模塊可以處理程序打包的事宜,它暴露出了打包相關(guān)的接口,該接口能夠被安裝程序和打包工具用來生成包含模塊字節(jié)碼的文件。同時,在開發(fā)環(huán)境中,compileall模塊也可以用來驗證源文件是否包含了語法錯誤。
在源代碼級別,pyclbr模塊提供了一個類查看器,方便文本編輯器或是其他程序?qū)ython程序中有意思的字符進(jìn)行掃描,比如函數(shù)或者是類。在提供了類查看器以后,就無需引入代碼,這樣就避免了潛在的副作用影響。
文檔字符串與doctest模塊
如果函數(shù),類或者是模塊的***行是一個字符串,那么這個字符串就是一個文檔字符串??梢哉J(rèn)為包含文檔字符串是一個良好的編程習(xí)慣,這是因為這些字符串可以給Python程序開發(fā)工具提供一些信息。比如,help()命令能夠檢測文檔字符串,Python相關(guān)的IDE也能夠進(jìn)行檢測文檔字符串的工作。由于程序員傾向于在交互式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
在編寫文檔時,一個常見的問題就是如何保持文檔和實際代碼的同步。例如,程序員也許會修改函數(shù)的實現(xiàn),但是卻忘記了更新文檔。針對這個問題,我們可以使用 doctest模塊。doctest模塊收集文檔字符串,并對它們進(jìn)行掃描,然后將它們作為測試進(jìn)行執(zhí)行。為了使用doctest模塊,我們通常會新建一 個用于測試的獨(dú)立的模塊。例如,如果前面的例子Test class包含在文件mult.py中,那么,你應(yīng)該新建一個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)會執(zhí)行特定模塊的測試,并且返回測試失敗的個數(shù)以及測試的總數(shù)目。如果所有的測試都通過了,那么不會產(chǎn)生任何輸出。否則的話,你將會看到一個失敗報告,用來顯示期望值和實際值之間的差別。如果你想看到測試的詳細(xì)輸出,你可以使用testmod(module, verbose=True).
如果不想新建一個單獨(dú)的測試文件的話,那么另一種選擇就是在文件末尾包含相應(yīng)的測試代碼:
- if __name__ == '__main__':
- import doctest
- doctest.testmod()
如果想執(zhí)行這類測試的話,我們可以通過-m選項調(diào)用doctest模塊。通常來講,當(dāng)執(zhí)行測試的時候沒有任何的輸出。如果想查看詳細(xì)信息的話,可以加上-v選項。
- $ python -m doctest -v mult.py
單元測試與unittest模塊
如果想更加徹底地 對程序進(jìn)行測試,我們可以使用unittest模塊。通過單元測試,開發(fā)者可以為構(gòu)成程序的每一個元素(例如,獨(dú)立的函數(shù),方法,類以及模塊)編寫一系列 獨(dú)立的測試用例。當(dāng)測試更大的程序時,這些測試就可以作為基石來驗證程序的正確性。當(dāng)我們的程序變得越來越大的時候,對不同構(gòu)件的單元測試就可以組合起來 成為更大的測試框架以及測試工具。這能夠極大地簡化軟件測試的工作,為找到并解決軟件問題提供了便利。
- # 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的類。在這個類里面,每一個測試都以方法的形式進(jìn)行定義,并都以test打頭進(jìn)行命名——例如,’testsimplestring‘,’testtypeconvert‘以及類似的命名方式(有必要強(qiáng)調(diào)一下,只要方法名以test打頭,那么無論怎么命名都是可以的)。在每個測試中,斷言可以用來對不同的條件進(jìn)行檢查。
實際的例子:
假如你在程序里有一個方法,這個方法的輸出指向標(biāo)準(zhǔn)輸出(sys.stdout)。這通常意味著是往屏幕上輸出文本信息。如果你想對你的代碼進(jìn)行測試來證明這一點,只要給出相應(yīng)的輸入,那么對應(yīng)的輸出就會被顯示出來。
- # url.py
- def urlprint(protocol, host, domain):
- url = '{}://{}.{}'.format(protocol, host, domain)
- print(url)
內(nèi)置的print函數(shù)在默認(rèn)情況下會往sys.stdout發(fā)送輸出。為了測試輸出已經(jīng)實際到達(dá),你可以使用一個替身對象對其進(jìn)行模擬,并且對程序的期望值進(jìn)行斷言。unittest.mock模塊中的patch()方法可以只在運(yùn)行測試的上下文中才替換對象,在測試完成后就立刻返回對象原始的狀態(tài)。下面是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()函數(shù)有三個參數(shù),測試代碼首先給每個參數(shù)賦了一個假值。變量expected_url包含了期望的輸出字符串。為了能夠執(zhí)行測試,我們使用了unittest.mock.patch()方法作為上下文管理器,把標(biāo)準(zhǔn)輸出sys.stdout替換為了StringIO對象,這樣發(fā)送的標(biāo)準(zhǔn)輸出的內(nèi)容就會被StringIO對象所接收。變量fake_out就是在這一過程中所創(chuàng)建出的模擬對象,該對象能夠在with所處的代碼塊中所使用,來進(jìn)行一系列的測試檢查。當(dāng)with語 句完成時,patch方法能夠?qū)⑺械臇|西都復(fù)原到測試執(zhí)行之前的狀態(tài),就好像測試沒有執(zhí)行一樣,而這無需任何額外的工作。但對于某些Python的C擴(kuò) 展來講,這個例子卻顯得毫無意義,這是因為這些C擴(kuò)展程序繞過了sys.stdout的設(shè)置,直接將輸出發(fā)送到了標(biāo)準(zhǔn)輸出上。這個例子僅適用于純 Python代碼的程序(如果你想捕獲到類似C擴(kuò)展的輸入輸出,那么你可以通過打開一個臨時文件然后將標(biāo)準(zhǔn)輸出重定向到該文件的技巧來進(jìn)行實現(xiàn))。
Python調(diào)試器與pdb模塊
Python在 pdb模塊中包含了一個簡單的基于命令行的調(diào)試器。pdb模塊支持事后調(diào)試(post-mortem debugging),棧幀探查(inspection of stack frames),斷點(breakpoints),單步調(diào)試(single-stepping of source lines)以及代碼審查(code evaluation)。
有好幾個函數(shù)都能夠在程序中調(diào)用調(diào)試器,或是在交互式的Python終端中進(jìn)行調(diào)試工作。
在所有啟動調(diào)試器的函數(shù)中,函數(shù)set_trace()也許是最簡易實用的了。如果在復(fù)雜程序中發(fā)現(xiàn)了問題,可以在代碼中插入set_trace()函數(shù),并運(yùn)行程序。當(dāng)執(zhí)行到set_trace()函數(shù)時,這就會暫停程序的執(zhí)行并直接跳轉(zhuǎn)到調(diào)試器中,這時候你就可以大展手腳開始檢查運(yùn)行時環(huán)境了。當(dāng)退出調(diào)試器時,調(diào)試器會自動恢復(fù)程序的執(zhí)行。
假設(shè)你的程序有問題,你想找到一個簡單的方法來對它進(jìn)行調(diào)試。
如果你的程序崩潰時報了一個異常錯誤,那么你可以用python3 -i someprogram.py這個命令來運(yùn)行你的程序,這能夠很好地發(fā)現(xiàn)問題所在。-i選項表明只要程序終結(jié)就立即啟動一個交互式shell。在這個交互式shell中,你就可以很好地探查到底發(fā)生了什么導(dǎo)致程序的錯誤。例如,如果你有以下代碼:
- def function(n):
- return n + 10
- function("Hello")
如果使用python3 -i 命令運(yùn)行程序就會產(chǎn)生如下輸出:
- python3 -i sample.py
- Traceback (most recent call last):
- File "sample.py", line 4, in <module>
- function("Hello")
- File "sample.py", line 2, in function
- return n + 10
- TypeError: Can't convert 'int' object to str implicitly
- >>> function(20)
- 30
- >>>
如果你沒有發(fā)現(xiàn)什么明顯的錯誤,那么你可以進(jìn)一步地啟動Python調(diào)試器。例如:
- >>> import pdb
- >>> pdb.pm()
- > sample.py(4)func()
- -> return n + 10
- (Pdb) w
- sample.py(6)<module>()
- -> func('Hello')
- > sample.py(4)func()
- -> return n + 10
- (Pdb) print n
- 'Hello'
- (Pdb) q
- >>>
如果你的代碼身處的環(huán)境很難啟動一個交互式shell的話(比如在服務(wù)器環(huán)境下),你可以增加錯誤處理的代碼,并自己輸出跟蹤信息。例如:
- import traceback
- import sys
- try:
- func(arg)
- except:
- print('**** AN ERROR OCCURRED ****')
- traceback.print_exc(file=sys.stderr)
如果你的程序并沒有崩潰,而是說程序的行為與你的預(yù)期表現(xiàn)的不一致,那么你可以嘗試在一些可能出錯的地方加入print()函數(shù)。如果你打算采用這種方案 的話,那么還有些相關(guān)的技巧值得探究。首先,函數(shù)traceback.print_stack()能夠在被執(zhí)行時立即打印出程序中棧的跟蹤信息。例如:
- >>> def sample(n):
- ... if n > 0:
- ... sample(n-1)
- ... else:
- ... traceback.print_stack(file=sys.stderr)
- ...
- >>> sample(5)
- File "<stdin>", line 1, in <module>
- File "<stdin>", line 3, in sample
- File "<stdin>", line 3, in sample
- File "<stdin>", line 3, in sample
- File "<stdin>", line 3, in sample
- File "<stdin>", line 3, in sample
- File "<stdin>", line 5, in sample
- >>>
另外,你可以在程序中任意一處使用pdb.set_trace()手動地啟動調(diào)試器,就像這樣:
- import pdb
- def func(arg):
- ...
- pdb.set_trace()
- ...
深入解析大型程序的時候,這是一個非常實用的技巧,這樣操作能夠清楚地了解程序的控制流或是函數(shù)的參數(shù)。比如,一旦調(diào)試器啟動了之后,你就可以使用print或者w命令來查看變量,來了解棧的跟蹤信息。
在進(jìn)行軟件調(diào)試時,千萬不要讓事情變得很復(fù)雜。有時候僅僅需要知道程序的跟蹤信息就能夠解決大部分的簡單錯誤(比如,實際的錯誤總是顯示在跟蹤信息的***一行)。在實際的開發(fā)過程中,將print()函數(shù)插入到代碼中也能夠很方便地顯示調(diào)試信息(只需要記得在調(diào)試完以后將print語句刪除掉就行了)。調(diào)試器的通用用法是在崩潰的函數(shù)中探查變量的值,知道如何在程序崩潰以后再進(jìn)入到調(diào)試器中就顯得非常實用。在程序的控制流不是那么清楚的情況下,你可以插入pdb.set_trace()語句來理清復(fù)雜程序的思路。本質(zhì)上,程序會一直執(zhí)行直到遇到set_trace()調(diào)用,之后程序就會立刻跳轉(zhuǎn)進(jìn)入到調(diào)試器中。在調(diào)試器里,你就可以進(jìn)行更多的嘗試。如果你正在使用Python的IDE,那么IDE通常會提供基于pdb的調(diào)試接口,你可以查閱IDE的相關(guān)文檔來獲取更多的信息。
下面是一些Python調(diào)試器入門的資源列表:
- 閱讀Steve Ferb的文章 “Debugging in Python”
- 觀看Eric Holscher的截圖 “Using pdb, the Python Debugger”
- 閱讀Ayman Hourieh的文章 “Python Debugging Techniques”
- 閱讀 Python documentation for pdb – The Python Debugger
- 閱讀Karen Tracey的D jango 1.1 Testing and Debugging一書中的第九章——When You Don’t Even Know What to Log: Using Debuggers
程序分析
profile模塊和cProfile模塊可以用來分析程序。它們的工作原理都一樣,唯一的區(qū)別是,cProfile模塊 是以C擴(kuò)展的方式實現(xiàn)的,如此一來運(yùn)行的速度也快了很多,也顯得比較流行。這兩個模塊都可以用來收集覆蓋信息(比如,有多少函數(shù)被執(zhí)行了),也能夠收集性 能數(shù)據(jù)。對一個程序進(jìn)行分析的最簡單的方法就是運(yùn)行這個命令:
- % python -m cProfile someprogram.py
此外,也可以使用profile模塊中的run函數(shù):
- run(command [, filename])
該函數(shù)會使用exec語句執(zhí)行command中的內(nèi)容。filename是可選的文件保存名,如果沒有filename的話,該命令的輸出會直接發(fā)送到標(biāo)準(zhǔn)輸出上。
下面是分析器執(zhí)行完成時的輸出報告:
- 126 function calls (6 primitive calls) in 5.130 CPU seconds
- Ordered by: standard name
- ncalls tottime percall cumtime percall filename:lineno(function)
- 1 0.030 0.030 5.070 5.070 <string>:1(?)
- 121/1 5.020 0.041 5.020 5.020 book.py:11(process)
- 1 0.020 0.020 5.040 5.040 book.py:5(?)
- 2 0.000 0.000 0.000 0.000 exceptions.py:101(_ _init_ _)
- 1 0.060 0.060 5.130 5.130 profile:0(execfile('book.py'))
- 0 0.000 0.000 profile:0(profiler)
當(dāng)輸出中的***列包含了兩個數(shù)字時(比如,121/1),后者是元調(diào)用(primitive call)的次數(shù),前者是實際調(diào)用的次數(shù)(譯者注:只有在遞歸情況下,實際調(diào)用的次數(shù)才會大于元調(diào)用的次數(shù),其他情況下兩者都相等)。對于絕大部分的應(yīng)用 程序來講使用該模塊所產(chǎn)生的的分析報告就已經(jīng)足夠了,比如,你只是想簡單地看一下你的程序花費(fèi)了多少時間。然后,如果你還想將這些數(shù)據(jù)保存下來,并在將來 對其進(jìn)行分析,你可以使用pstats模塊。
假設(shè)你想知道你的程序究竟在哪里花費(fèi)了多少時間。
如果你只是想簡單地給你的整個程序計時的話,使用Unix中的time命令就已經(jīng)完全能夠應(yīng)付了。例如:
- bash % time python3 someprogram.py
- real 0m13.937s
- user 0m12.162s
- sys 0m0.098s
- bash %
通常來講,分析代碼的程度會介于這兩個極端之間。比如,你可能已經(jīng)知道你的代碼會在一些特定的函數(shù)中花的時間特別多。針對這類特定函數(shù)的分析,我們可以使用修飾器decorator,例如:
- import time
- from functools import wraps
- def timethis(func):
- @wraps(func)
- def wrapper(*args, **kwargs):
- start = time.perf_counter()
- r = func(*args, **kwargs)
- end = time.perf_counter()
- print('{}.{} : {}'.format(func.__module__, func.__name__, end - start))
- return r
- return wrapper
使用decorator的方式很簡單,你只需要把它放在你想要分析的函數(shù)的定義前面就可以了。例如:
- >>> @timethis
- ... def countdown(n):
- ... while n > 0:
- ... n -= 1
- ...
- >>> countdown(10000000)
- __main__.countdown : 0.803001880645752
- >>>
如果想要分析一個語句塊的話,你可以定義一個上下文管理器(context manager)。例如:
- import time
- from contextlib import contextmanager
- @contextmanager
- def timeblock(label):
- start = time.perf_counter()
- try:
- yield
- finally:
- end = time.perf_counter()
- print('{} : {}'.format(label, end - start))
接下來是如何使用上下文管理器的例子:
- >>> with timeblock('counting'):
- ... n = 10000000
- ... while n > 0:
- ... n -= 1
- ...
- counting : 1.5551159381866455
- >>>