天天看點

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中檢視文檔字元串,是以最好将這些字元串寫的簡短一些。例如

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

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

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

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

單元測試與unittest子產品

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

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

實際的例子:

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

内置的print函數在預設情況下會往sys.stdout發送輸出。為了測試輸出已經實際到達,你可以使用一個替身對象對其進行模拟,并且對程式的期望值進行斷言。unittest.mock子產品中的patch()方法可以隻在運作測試的上下文中才替換對象,在測試完成後就立刻傳回對象原始的狀态。下面是urlprint()方法的測試代碼:

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中,你就可以很好地探查到底發生了什麼導緻程式的錯誤。例如,如果你有以下代碼:

如果你沒有發現什麼明顯的錯誤,那麼你可以進一步地啟動python調試器。例如:

如果你的代碼身處的環境很難啟動一個互動式shell的話(比如在伺服器環境下),你可以增加錯誤處理的代碼,并自己輸出跟蹤資訊。例如:

如果你的程式并沒有崩潰,而是說程式的行為與你的預期表現的不一緻,那麼你可以嘗試在一些可能出錯的地方加入print()函數。如果你打算采用這種方案的話,那麼還有些相關的技巧值得探究。首先,函數traceback.print_stack()能夠在被執行時立即列印出程式中棧的跟蹤資訊。例如:

另外,你可以在程式中任意一處使用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擴充的方式實作的,如此一來運作的速度也快了很多,也顯得比較流行。這兩個子產品都可以用來收集覆寫資訊(比如,有多少函數被執行了),也能夠收集性能資料。對一個程式進行分析的最簡單的方法就是運作這個指令:

該函數會使用exec語句執行command中的内容。filename是可選的檔案儲存名,如果沒有filename的話,該指令的輸出會直接發送到标準輸出上。

下面是分析器執行完成時的輸出報告:

當輸出中的第一列包含了兩個數字時(比如,121/1),後者是元調用(primitive call)的次數,前者是實際調用的次數(譯者注:隻有在遞歸情況下,實際調用的次數才會大于元調用的次數,其他情況下兩者都相等)。對于絕大部分的應用程式來講使用該子產品所産生的的分析報告就已經足夠了,比如,你隻是想簡單地看一下你的程式花費了多少時間。然後,如果你還想将這些資料儲存下來,并在将來對其進行分析,你可以使用pstats子產品。

假設你想知道你的程式究竟在哪裡花費了多少時間。

如果你隻是想簡單地給你的整個程式計時的話,使用unix中的time指令就已經完全能夠應付了。例如:

通常來講,分析代碼的程度會介于這兩個極端之間。比如,你可能已經知道你的代碼會在一些特定的函數中花的時間特别多。針對這類特定函數的分析,我們可以使用修飾器decorator,例如:

使用decorator的方式很簡單,你隻需要把它放在你想要分析的函數的定義前面就可以了。例如:

如果想要分析一個語句塊的話,你可以定義一個上下文管理器(context manager)。例如:

接下來是如何使用上下文管理器的例子:

如果想研究一小段代碼的性能的話,timeit子產品會非常有用。例如:

timeit的工作原理是,将第一個參數中的語句執行100萬次,然後計算所花費的時間。第二個參數指定了一些測試之前需要做的環境準備工作。如果你需要改變疊代的次數,可以附加一個number參數,就像這樣:

當進行性能評估的時候,要牢記任何得出的結果隻是一個估算值。函數time.perf_counter()能夠在任一平台提供最高精度的計時器。然而,它也隻是記錄了自然時間,記錄自然時間會被很多其他因素影響,比如,計算機的負載。如果你對處理時間而非自然時間感興趣的話,你可以使用time.process_time()。例如:

最後也是相當重要的就是,如果你想做一個詳細的性能評估的話,你最好查閱time,timeit以及其他相關子產品的文檔,這樣你才能夠對平台相關的不同之處有所了解。

profile子產品中最基礎的東西就是run()函數了。該函數會把一個語句字元串作為參數,然後在執行語句時生成所花費的時間報告。

性能優化

當你的程式運作地很慢的時候,你就會想去提升它的運作速度,但是你又不想去借用一些複雜方案的幫助,比如使用c擴充或是just-in-time(jit)編譯器。

那麼這時候應該怎麼辦呢?要牢記性能優化的第一要義就是“不要為了優化而去優化,應該在我們開始寫代碼之前就想好應該怎樣編寫高性能的代碼”。第二要義就是“優化一定要抓住重點,找到程式中最重要的地方去優化,而不要去優化那些不重要的部分”。

通常來講,你會發現你的程式在某些熱點上花費了很多時間,比如内部資料的循環處理。一旦你發現了問題所在,你就可以對症下藥,讓你的程式更快地執行。

使用函數

許多開發者剛開始的時候會将python作為一個編寫簡單腳本的工具。當編寫腳本的時候,很容易就會寫一些沒有結構的代碼出來。例如:

但是,卻很少有人知道,定義在全局範圍内的代碼要比定義在函數中的代碼執行地慢。他們之間速度的差别是因為局部變量與全局變量不同的實作所引起的(局部變量的操作要比全局變量來得快)。是以,如果你想要讓程式更快地運作,那麼你可以簡單地将代碼放在一個函數中,就像這樣:

這樣操作以後,處理速度會有提升,但是這個提升的程度依賴于程式的複雜性。根據經驗來講,通常都會提升15%到30%之間。

選擇性地減少屬性的通路

當使用點(.)操作符去通路屬性時都會帶來一定的消耗。本質上來講,這會觸發一些特殊方法的執行,比如__getattribute__()和__getattr__(),這通常都會導緻去記憶體中字典資料的查詢。

你可以通過兩種方式來避免屬性的通路,第一種是使用from module import name的方式。第二種是将對象的方法名儲存下來,在調用時直接使用。為了解釋地更加清楚,我們來看一個例子:

上面的代碼在我的計算機上運作大概需要40秒的時間。現在我們把上面代碼中的compute_roots()函數改寫一下:

這個版本的代碼執行一下大概需要29秒。這兩個版本的代碼唯一的不同之處在于後面一個版本減少了對屬性的通路。在後面一段代碼中,我們使用了sqrt()方法,而非math.sqrt()。result.append()函數也被存進了一個局部變量result_append中,然後在循環當中重複使用。

然而,有必要強調一點是說,這種方式的優化僅僅針對經常運作的代碼有效,比如循環。由此可見,優化僅僅在那些小心挑選出來的地方才會真正得到展現。

了解變量的局部性

上面已經講過,局部變量的操作比全局變量來得快。對于經常要通路的變量來說,最好把他們儲存成局部變量。例如,考慮剛才已經讨論過的compute_roots()函數修改版:

在這個版本中,sqrt函數被一個局部變量所替代。如果你執行這段代碼的話,大概需要25秒就執行完了(前一個版本需要29秒)。 這次速度的提升是因為sqrt局部變量的查詢比sqrt函數的全局查詢來得稍快。

局部性原來同樣适用于類的參數。通常來講,使用self.name要比直接通路局部變量來得慢。在内部循環中,我們可以将經常要通路的屬性儲存為一個局部變量。例如:

避免不必要的抽象

任何時候當你想給你的代碼添加其他處理邏輯,比如添加裝飾器,屬性或是描述符,你都是在拖慢你的程式。例如,考慮這樣一個類:

現在,讓我們簡單地測試一下:

正如你所看到的,我們通路屬性y比通路簡單屬性x不是慢了一點點,整整慢了4.5倍之多。如果你在乎性能的話,你就很有必要問一下你自己,對y的那些額外的定義是否都是必要的了。如果不是的話,那麼你應該把那些額外的定義删掉,用一個簡單的屬性就夠了。如果隻是因為在其他語言裡面經常使用getter和setter函數的話,你完全沒有必要在python中也使用相同的編碼風格。

使用内置的容器

内置的資料結構,例如字元串(string),元組(tuple),清單(list),集合(set)以及字典(dict)都是用c語言實作的,正是因為采用了c來實作,是以它們的性能表現也很好。如果你傾向于使用你自己的資料結構作為替代的話(例如,連結清單,平衡樹或是其他資料結構),想達到内置資料結構的速度的話是非常困難的。是以,你應該盡可能地使用内置的資料結構。

避免不必要的資料結構或是資料拷貝

有時候程式員會有點兒走神,在不該用到資料結構的地方去用資料結構。例如,有人可能會寫這樣的的代碼:

也許他這麼寫是為了先得到一個清單,然後再在這個清單上進行一些操作。但是第一個清單是完全沒有必要寫在這裡的。我們可以簡單地把代碼寫成這樣就行了:

有鑒于此,你要小心那些偏執程式員所寫的代碼了,這些程式員對python的值共享機制非常偏執。函數copy.deepcopy()的濫用也許是一個信号,表明該代碼是由菜鳥或者是不相信python記憶體模型的人所編寫的。在這樣的代碼裡,減少copy的使用也許會比較安全。

在優化之前,很有必要先詳細了解一下你所要使用的算法。如果你能夠将算法的複雜度從o(n^2)降為o(n log n)的話,程式的性能将得到極大的提高。

如果你已經打算進行優化工作了,那就很有必要全局地考慮一下。普适的原則就是,不要想去優化程式的每一個部分,這是因為優化工作會讓代碼變得晦澀難懂。相反,你應該把注意力集中在已知的性能瓶頸處,例如内部循環。

你需要謹慎地對待微優化(micro-optimization)的結果。例如,考慮下面兩種建立字典結構的方式:

後面那一種方式打字打的更少一些(因為你不必将key的名字用雙引号括起來)。然而當你将這兩種編碼方式進行性能對比時,你會發現使用dict()函數的方式比另一種慢了3倍之多!知道了這一點以後,你也許會傾向于掃描你的代碼,把任何出現dict()的地方替換為另一種備援的寫法。然而,一個聰明的程式員絕對不會這麼做,他隻會将注意力放在值得關注的地方,比如在循環上。在其他地方,速度的差異并不是最重要的。但是,如果你想讓你的程式性能有質的飛躍的話,你可以去研究下基于jit技術的工具。比如,pypy項目,該項目是python解釋器的另一種實作,它能夠分析程式的執行并為經常執行的代碼生成機器碼,有時它甚至能夠讓python程式的速度提升一個數量級,達到(甚至超過)c語言編寫的代碼的速度。但是不幸的是,在本文正在寫的時候,pypy還沒有完全支援python 3。是以,我們還是在将來再來看它到底會發展的怎麼樣。基于jit技術的還有numba項目。該項目實作的是一個動态的編譯器,你可以将你想要優化的python函數以注解的方式進行标記,然後這些代碼就會在llvm的幫助下被編譯成機器碼。該項目也能夠帶來極大的性能上的提升。然而,就像pypy一樣,該項目對python 3的支援還隻是實驗性的。

最後,但是也很重要的是,請牢記john ousterhout(譯者注:tcl和tk的發明者,現為斯坦福大學計算機系的教授)說過的話“将不工作的東西變成能夠工作的,這才是最大的性能提升”。在你需要優化前不要過分地考慮程式的優化工作。程式的正确性通常來講都比程式的性能要來的重要。

原文連結: pypix.com 翻譯: 伯樂線上 - brightconan

繼續閱讀