Python 程序員必知必會的開發者工具
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擴展的輸入輸出,那么你可以通過打開一個臨時文件然后將標準輸出重定向到該文件的技巧來進行實現)。
Python調試器與pdb模塊
Python在 pdb模塊中包含了一個簡單的基于命令行的調試器。pdb模塊支持事后調試(post-mortem debugging),棧幀探查(inspection of stack frames),斷點(breakpoints),單步調試(single-stepping of source lines)以及代碼審查(code evaluation)。
有好幾個函數都能夠在程序中調用調試器,或是在交互式的Python終端中進行調試工作。
在所有啟動調試器的函數中,函數set_trace()也許是最簡易實用的了。如果在復雜程序中發現了問題,可以在代碼中插入set_trace()函數,并運行程序。當執行到set_trace()函數時,這就會暫停程序的執行并直接跳轉到調試器中,這時候你就可以大展手腳開始檢查運行時環境了。當退出調試器時,調試器會自動恢復程序的執行。
假設你的程序有問題,你想找到一個簡單的方法來對它進行調試。
如果你的程序崩潰時報了一個異常錯誤,那么你可以用python3 -i someprogram.py這個命令來運行你的程序,這能夠很好地發現問題所在。-i選項表明只要程序終結就立即啟動一個交互式shell。在這個交互式shell中,你就可以很好地探查到底發生了什么導致程序的錯誤。例如,如果你有以下代碼:
- def function(n):
- return n + 10
- function("Hello")
如果使用python3 -i 命令運行程序就會產生如下輸出:
- 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
- >>>
如果你沒有發現什么明顯的錯誤,那么你可以進一步地啟動Python調試器。例如:
- >>> 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
- >>>
如果你的代碼身處的環境很難啟動一個交互式shell的話(比如在服務器環境下),你可以增加錯誤處理的代碼,并自己輸出跟蹤信息。例如:
- import traceback
- import sys
- try:
- func(arg)
- except:
- print('**** AN ERROR OCCURRED ****')
- traceback.print_exc(file=sys.stderr)
如果你的程序并沒有崩潰,而是說程序的行為與你的預期表現的不一致,那么你可以嘗試在一些可能出錯的地方加入print()函數。如果你打算采用這種方案 的話,那么還有些相關的技巧值得探究。首先,函數traceback.print_stack()能夠在被執行時立即打印出程序中棧的跟蹤信息。例如:
- >>> 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()手動地啟動調試器,就像這樣:
- import pdb
- def func(arg):
- ...
- pdb.set_trace()
- ...
深入解析大型程序的時候,這是一個非常實用的技巧,這樣操作能夠清楚地了解程序的控制流或是函數的參數。比如,一旦調試器啟動了之后,你就可以使用print或者w命令來查看變量,來了解棧的跟蹤信息。
在進行軟件調試時,千萬不要讓事情變得很復雜。有時候僅僅需要知道程序的跟蹤信息就能夠解決大部分的簡單錯誤(比如,實際的錯誤總是顯示在跟蹤信息的***一行)。在實際的開發過程中,將print()函數插入到代碼中也能夠很方便地顯示調試信息(只需要記得在調試完以后將print語句刪除掉就行了)。調試器的通用用法是在崩潰的函數中探查變量的值,知道如何在程序崩潰以后再進入到調試器中就顯得非常實用。在程序的控制流不是那么清楚的情況下,你可以插入pdb.set_trace()語句來理清復雜程序的思路。本質上,程序會一直執行直到遇到set_trace()調用,之后程序就會立刻跳轉進入到調試器中。在調試器里,你就可以進行更多的嘗試。如果你正在使用Python的IDE,那么IDE通常會提供基于pdb的調試接口,你可以查閱IDE的相關文檔來獲取更多的信息。
下面是一些Python調試器入門的資源列表:
- 閱讀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模塊可以用來分析程序。它們的工作原理都一樣,唯一的區別是,cProfile模塊 是以C擴展的方式實現的,如此一來運行的速度也快了很多,也顯得比較流行。這兩個模塊都可以用來收集覆蓋信息(比如,有多少函數被執行了),也能夠收集性 能數據。對一個程序進行分析的最簡單的方法就是運行這個命令:
- % python -m cProfile someprogram.py
此外,也可以使用profile模塊中的run函數:
- run(command [, filename])
該函數會使用exec語句執行command中的內容。filename是可選的文件保存名,如果沒有filename的話,該命令的輸出會直接發送到標準輸出上。
下面是分析器執行完成時的輸出報告:
- 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)
當輸出中的***列包含了兩個數字時(比如,121/1),后者是元調用(primitive call)的次數,前者是實際調用的次數(譯者注:只有在遞歸情況下,實際調用的次數才會大于元調用的次數,其他情況下兩者都相等)。對于絕大部分的應用 程序來講使用該模塊所產生的的分析報告就已經足夠了,比如,你只是想簡單地看一下你的程序花費了多少時間。然后,如果你還想將這些數據保存下來,并在將來 對其進行分析,你可以使用pstats模塊。
假設你想知道你的程序究竟在哪里花費了多少時間。
如果你只是想簡單地給你的整個程序計時的話,使用Unix中的time命令就已經完全能夠應付了。例如:
- bash % time python3 someprogram.py
- real 0m13.937s
- user 0m12.162s
- sys 0m0.098s
- bash %
通常來講,分析代碼的程度會介于這兩個極端之間。比如,你可能已經知道你的代碼會在一些特定的函數中花的時間特別多。針對這類特定函數的分析,我們可以使用修飾器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的方式很簡單,你只需要把它放在你想要分析的函數的定義前面就可以了。例如:
- >>> @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
- >>>