摘要:通過單元測試,開發(fā)者可以為構(gòu)成程序的每一個(gè)元素例如,獨(dú)立的函數(shù),方法,類以及模塊編寫一系列獨(dú)立的測試用例。在每個(gè)測試中,斷言可以用來對不同的條件進(jìn)行檢查。當(dāng)退出調(diào)試器時(shí),調(diào)試器會(huì)自動(dòng)恢復(fù)程序的執(zhí)行。
Python已經(jīng)演化出了一個(gè)廣泛的生態(tài)系統(tǒng),該生態(tài)系統(tǒng)能夠讓Python程序員的生活變得更加簡單,減少他們重復(fù)造輪的工作。同樣的理念也適用于工具開發(fā)者的工作,即便他們開發(fā)出的工具并沒有出現(xiàn)在最終的程序中。本文將介紹Python程序員必知必會(huì)的開發(fā)者工具。
對于開發(fā)者來說,最實(shí)用的幫助莫過于幫助他們編寫代碼文檔了。pydoc模塊可以根據(jù)源代碼中的docstrings為任何可導(dǎo)入模塊生成格式良好的文檔。Python包含了兩個(gè)測試框架來自動(dòng)測試代碼以及驗(yàn)證代碼的正確性:1)doctest模塊,該模塊可以從源代碼或獨(dú)立文件的例子中抽取出測試用例。2)unittest模塊,該模塊是一個(gè)全功能的自動(dòng)化測試框架,該框架提供了對測試準(zhǔn)備(test fixtures), 預(yù)定義測試集(predefined test suite)以及測試發(fā)現(xiàn)(test discovery)的支持。
trace模塊可以監(jiān)控Python執(zhí)行程序的方式,同時(shí)生成一個(gè)報(bào)表來顯示程序的每一行執(zhí)行的次數(shù)。這些信息可以用來發(fā)現(xiàn)未被自動(dòng)化測試集所覆蓋的程序執(zhí)行路徑,也可以用來研究程序調(diào)用圖,進(jìn)而發(fā)現(xiàn)模塊之間的依賴關(guān)系。編寫并執(zhí)行測試可以發(fā)現(xiàn)絕大多數(shù)程序中的問題,Python使得debug工作變得更加簡單,這是因?yàn)樵诖蟛糠智闆r下,Python都能夠?qū)⑽幢惶幚淼腻e(cuò)誤打印到控制臺中,我們稱這些錯(cuò)誤信息為traceback。如果程序不是在文本控制臺中運(yùn)行的,traceback也能夠?qū)㈠e(cuò)誤信息輸出到日志文件或是消息對話框中。當(dāng)標(biāo)準(zhǔn)的traceback無法提供足夠的信息時(shí),可以使用cgitb 模塊來查看各級棧和源代碼上下文中的詳細(xì)信息,比如局部變量。cgitb模塊還能夠?qū)⑦@些跟蹤信息以HTML的形式輸出,用來報(bào)告web應(yīng)用中的錯(cuò)誤。
一旦發(fā)現(xiàn)了問題出在哪里后,就需要使用到交互式調(diào)試器進(jìn)入到代碼中進(jìn)行調(diào)試工作了,pdb模塊能夠很好地勝任這項(xiàng)工作。該模塊可以顯示出程序在錯(cuò)誤產(chǎn)生時(shí)的執(zhí)行路徑,同時(shí)可以動(dòng)態(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é)碼編譯版本。這個(gè)字節(jié)碼編譯版本可以在程序執(zhí)行時(shí)動(dòng)態(tài)地生成,也可以在程序打包的時(shí)候就生成。compileall 模塊可以處理程序打包的事宜,它暴露出了打包相關(guān)的接口,該接口能夠被安裝程序和打包工具用來生成包含模塊字節(jié)碼的文件。同時(shí),在開發(fā)環(huán)境中,compileall模塊也可以用來驗(yàn)證源文件是否包含了語法錯(cuò)誤。
在源代碼級別,pyclbr 模塊提供了一個(gè)類查看器,方便文本編輯器或是其他程序?qū)ython程序中有意思的字符進(jìn)行掃描,比如函數(shù)或者是類。在提供了類查看器以后,就無需引入代碼,這樣就避免了潛在的副作用影響。
文檔字符串與doctest模塊如果函數(shù),類或者是模塊的第一行是一個(gè)字符串,那么這個(gè)字符串就是一個(gè)文檔字符串。可以認(rèn)為包含文檔字符串是一個(gè)良好的編程習(xí)慣,這是因?yàn)檫@些字符串可以給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í),一個(gè)常見的問題就是如何保持文檔和實(shí)際代碼的同步。例如,程序員也許會(huì)修改函數(shù)的實(shí)現(xiàn),但是卻忘記了更新文檔。針對這個(gè)問題,我們可以使用doctest模塊。doctest模塊收集文檔字符串,并對它們進(jìn)行掃描,然后將它們作為測試進(jìn)行執(zhí)行。為了使用doctest模塊,我們通常會(huì)新建一個(gè)用于測試的獨(dú)立的模塊。例如,如果前面的例子Test class包含在文件mult.py中,那么,你應(yīng)該新建一個(gè)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)會(huì)執(zhí)行特定模塊的測試,并且返回測試失敗的個(gè)數(shù)以及測試的總數(shù)目。如果所有的測試都通過了,那么不會(huì)產(chǎn)生任何輸出。否則的話,你將會(huì)看到一個(gè)失敗報(bào)告,用來顯示期望值和實(shí)際值之間的差別。如果你想看到測試的詳細(xì)輸出,你可以使用testmod(module, verbose=True).
如果不想新建一個(gè)多帶帶的測試文件的話,那么另一種選擇就是在文件末尾包含相應(yīng)的測試代碼:
if __name__ == "__main__": import doctest doctest.testmod()
如果想執(zhí)行這類測試的話,我們可以通過-m選項(xiàng)調(diào)用doctest模塊。通常來講,當(dāng)執(zhí)行測試的時(shí)候沒有任何的輸出。如果想查看詳細(xì)信息的話,可以加上-v選項(xiàng)。
$ python -m doctest -v mult.py單元測試與unittest模塊
如果想更加徹底地對程序進(jìn)行測試,我們可以使用unittest模塊。通過單元測試,開發(fā)者可以為構(gòu)成程序的每一個(gè)元素(例如,獨(dú)立的函數(shù),方法,類以及模塊)編寫一系列獨(dú)立的測試用例。當(dāng)測試更大的程序時(shí),這些測試就可以作為基石來驗(yàn)證程序的正確性。當(dāng)我們的程序變得越來越大的時(shí)候,對不同構(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
在使用單元測試時(shí),我們需要定義一個(gè)繼承自unittest.TestCase的類。在這個(gè)類里面,每一個(gè)測試都以方法的形式進(jìn)行定義,并都以test打頭進(jìn)行命名——例如,’testsimplestring‘,’testtypeconvert‘以及類似的命名方式(有必要強(qiáng)調(diào)一下,只要方法名以test打頭,那么無論怎么命名都是可以的)。在每個(gè)測試中,斷言可以用來對不同的條件進(jìn)行檢查。
實(shí)際的例子:
假如你在程序里有一個(gè)方法,這個(gè)方法的輸出指向標(biāo)準(zhǔn)輸出(sys.stdout)。這通常意味著是往屏幕上輸出文本信息。如果你想對你的代碼進(jìn)行測試來證明這一點(diǎn),只要給出相應(yīng)的輸入,那么對應(yīng)的輸出就會(huì)被顯示出來。
# url.py def urlprint(protocol, host, domain): url = "{}://{}.{}".format(protocol, host, domain) print(url)
內(nèi)置的print函數(shù)在默認(rèn)情況下會(huì)往sys.stdout發(fā)送輸出。為了測試輸出已經(jīng)實(shí)際到達(dá),你可以使用一個(gè)替身對象對其進(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 = "{}://{}.{} ".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ù)有三個(gè)參數(shù),測試代碼首先給每個(gè)參數(shù)賦了一個(gè)假值。變量expected_url包含了期望的輸出字符串。為了能夠執(zhí)行測試,我們使用了unittest.mock.patch()方法作為上下文管理器,把標(biāo)準(zhǔn)輸出sys.stdout替換為了StringIO對象,這樣發(fā)送的標(biāo)準(zhǔn)輸出的內(nèi)容就會(huì)被StringIO對象所接收。變量fake_out就是在這一過程中所創(chuàng)建出的模擬對象,該對象能夠在with所處的代碼塊中所使用,來進(jìn)行一系列的測試檢查。當(dāng)with語句完成時(shí),patch方法能夠?qū)⑺械臇|西都復(fù)原到測試執(zhí)行之前的狀態(tài),就好像測試沒有執(zhí)行一樣,而這無需任何額外的工作。但對于某些Python的C擴(kuò)展來講,這個(gè)例子卻顯得毫無意義,這是因?yàn)檫@些C擴(kuò)展程序繞過了sys.stdout的設(shè)置,直接將輸出發(fā)送到了標(biāo)準(zhǔn)輸出上。這個(gè)例子僅適用于純Python代碼的程序(如果你想捕獲到類似C擴(kuò)展的輸入輸出,那么你可以通過打開一個(gè)臨時(shí)文件然后將標(biāo)準(zhǔn)輸出重定向到該文件的技巧來進(jìn)行實(shí)現(xiàn))。
Python調(diào)試器與pdb模塊Python在pdb模塊中包含了一個(gè)簡單的基于命令行的調(diào)試器。pdb模塊支持事后調(diào)試(post-mortem debugging),棧幀探查(inspection of stack frames),斷點(diǎn)(breakpoints),單步調(diào)試(single-stepping of source lines)以及代碼審查(code evaluation)。
有好幾個(gè)函數(shù)都能夠在程序中調(diào)用調(diào)試器,或是在交互式的Python終端中進(jìn)行調(diào)試工作。
在所有啟動(dòng)調(diào)試器的函數(shù)中,函數(shù)set_trace()也許是最簡易實(shí)用的了。如果在復(fù)雜程序中發(fā)現(xiàn)了問題,可以在代碼中插入set_trace()函數(shù),并運(yùn)行程序。當(dāng)執(zhí)行到set_trace()函數(shù)時(shí),這就會(huì)暫停程序的執(zhí)行并直接跳轉(zhuǎn)到調(diào)試器中,這時(shí)候你就可以大展手腳開始檢查運(yùn)行時(shí)環(huán)境了。當(dāng)退出調(diào)試器時(shí),調(diào)試器會(huì)自動(dòng)恢復(fù)程序的執(zhí)行。
假設(shè)你的程序有問題,你想找到一個(gè)簡單的方法來對它進(jìn)行調(diào)試。
如果你的程序崩潰時(shí)報(bào)了一個(gè)異常錯(cuò)誤,那么你可以用python3 -i someprogram.py這個(gè)命令來運(yùn)行你的程序,這能夠很好地發(fā)現(xiàn)問題所在。-i選項(xiàng)表明只要程序終結(jié)就立即啟動(dòng)一個(gè)交互式shell。在這個(gè)交互式shell中,你就可以很好地探查到底發(fā)生了什么導(dǎo)致程序的錯(cuò)誤。例如,如果你有以下代碼:
def function(n): return n + 10 function("Hello")
如果使用python3 -i 命令運(yùn)行程序就會(huì)產(chǎn)生如下輸出:
python3 -i sample.py Traceback (most recent call last): File "sample.py", line 4, infunction("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)什么明顯的錯(cuò)誤,那么你可以進(jìn)一步地啟動(dòng)Python調(diào)試器。例如:
>>> import pdb >>> pdb.pm() > sample.py(4)func() -> return n + 10 (Pdb) w sample.py(6)() -> func("Hello") > sample.py(4)func() -> return n + 10 (Pdb) print n "Hello" (Pdb) q >>>
如果你的代碼身處的環(huán)境很難啟動(dòng)一個(gè)交互式shell的話(比如在服務(wù)器環(huán)境下),你可以增加錯(cuò)誤處理的代碼,并自己輸出跟蹤信息。例如:
import traceback import sys try: func(arg) except: print("**** AN ERROR OCCURRED ****") traceback.print_exc(file=sys.stderr)
如果你的程序并沒有崩潰,而是說程序的行為與你的預(yù)期表現(xiàn)的不一致,那么你可以嘗試在一些可能出錯(cuò)的地方加入print()函數(shù)。如果你打算采用這種方案的話,那么還有些相關(guān)的技巧值得探究。首先,函數(shù)traceback.print_stack()能夠在被執(zhí)行時(shí)立即打印出程序中棧的跟蹤信息。例如:
>>> def sample(n): ... if n > 0: ... sample(n-1) ... else: ... traceback.print_stack(file=sys.stderr) ... >>> sample(5) File "", line 1, in File " ", line 3, in sample File " ", line 3, in sample File " ", line 3, in sample File " ", line 3, in sample File " ", line 3, in sample File " ", line 5, in sample >>>
另外,你可以在程序中任意一處使用pdb.set_trace()手動(dòng)地啟動(dòng)調(diào)試器,就像這樣:
import pdb def func(arg): ... pdb.set_trace() ...
在深入解析大型程序的時(shí)候,這是一個(gè)非常實(shí)用的技巧,這樣操作能夠清楚地了解程序的控制流或是函數(shù)的參數(shù)。比如,一旦調(diào)試器啟動(dòng)了之后,你就可以使用print或者w命令來查看變量,來了解棧的跟蹤信息。
在進(jìn)行軟件調(diào)試時(shí),千萬不要讓事情變得很復(fù)雜。有時(shí)候僅僅需要知道程序的跟蹤信息就能夠解決大部分的簡單錯(cuò)誤(比如,實(shí)際的錯(cuò)誤總是顯示在跟蹤信息的最后一行)。在實(shí)際的開發(fā)過程中,將print()函數(shù)插入到代碼中也能夠很方便地顯示調(diào)試信息(只需要記得在調(diào)試完以后將print語句刪除掉就行了)。調(diào)試器的通用用法是在崩潰的函數(shù)中探查變量的值,知道如何在程序崩潰以后再進(jìn)入到調(diào)試器中就顯得非常實(shí)用。在程序的控制流不是那么清楚的情況下,你可以插入pdb.set_trace()語句來理清復(fù)雜程序的思路。本質(zhì)上,程序會(huì)一直執(zhí)行直到遇到set_trace()調(diào)用,之后程序就會(huì)立刻跳轉(zhuǎn)進(jìn)入到調(diào)試器中。在調(diào)試器里,你就可以進(jìn)行更多的嘗試。如果你正在使用Python的IDE,那么IDE通常會(huì)提供基于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ò)展的方式實(shí)現(xiàn)的,如此一來運(yùn)行的速度也快了很多,也顯得比較流行。這兩個(gè)模塊都可以用來收集覆蓋信息(比如,有多少函數(shù)被執(zhí)行了),也能夠收集性能數(shù)據(jù)。對一個(gè)程序進(jìn)行分析的最簡單的方法就是運(yùn)行這個(gè)命令:
% python -m cProfile someprogram.py
此外,也可以使用profile模塊中的run函數(shù):
run(command [, filename])
該函數(shù)會(huì)使用exec語句執(zhí)行command中的內(nèi)容。filename是可選的文件保存名,如果沒有filename的話,該命令的輸出會(huì)直接發(fā)送到標(biāo)準(zhǔn)輸出上。
下面是分析器執(zhí)行完成時(shí)的輸出報(bào)告:
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: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)輸出中的第一列包含了兩個(gè)數(shù)字時(shí)(比如,121/1),后者是元調(diào)用(primitive call)的次數(shù),前者是實(shí)際調(diào)用的次數(shù)(譯者注:只有在遞歸情況下,實(shí)際調(diào)用的次數(shù)才會(huì)大于元調(diào)用的次數(shù),其他情況下兩者都相等)。對于絕大部分的應(yīng)用程序來講使用該模塊所產(chǎn)生的的分析報(bào)告就已經(jīng)足夠了,比如,你只是想簡單地看一下你的程序花費(fèi)了多少時(shí)間。然后,如果你還想將這些數(shù)據(jù)保存下來,并在將來對其進(jìn)行分析,你可以使用pstats模塊。
假設(shè)你想知道你的程序究竟在哪里花費(fèi)了多少時(shí)間。
如果你只是想簡單地給你的整個(gè)程序計(jì)時(shí)的話,使用Unix中的time命令就已經(jīng)完全能夠應(yīng)付了。例如:
bash % time python3 someprogram.py real 0m13.937s user 0m12.162s sys 0m0.098s bash %
通常來講,分析代碼的程度會(huì)介于這兩個(gè)極端之間。比如,你可能已經(jīng)知道你的代碼會(huì)在一些特定的函數(shù)中花的時(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 >>>
如果想要分析一個(gè)語句塊的話,你可以定義一個(gè)上下文管理器(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 >>>
如果想研究一小段代碼的性能的話,timeit模塊會(huì)非常有用。例如:
>>> from timeit import timeit >>> timeit("math.sqrt(2)", "import math") 0.1432319980012835 >>> timeit("sqrt(2)", "from math import sqrt") 0.10836604500218527 >>>
timeit的工作原理是,將第一個(gè)參數(shù)中的語句執(zhí)行100萬次,然后計(jì)算所花費(fèi)的時(shí)間。第二個(gè)參數(shù)指定了一些測試之前需要做的環(huán)境準(zhǔn)備工作。如果你需要改變迭代的次數(shù),可以附加一個(gè)number參數(shù),就像這樣:
>>> timeit("math.sqrt(2)", "import math", number=10000000) 1.434852126003534 >>> timeit("sqrt(2)", "from math import sqrt", number=10000000) 1.0270336690009572 >>>
當(dāng)進(jìn)行性能評估的時(shí)候,要牢記任何得出的結(jié)果只是一個(gè)估算值。函數(shù)time.perf_counter()能夠在任一平臺提供最高精度的計(jì)時(shí)器。然而,它也只是記錄了自然時(shí)間,記錄自然時(shí)間會(huì)被很多其他因素影響,比如,計(jì)算機(jī)的負(fù)載。如果你對處理時(shí)間而非自然時(shí)間感興趣的話,你可以使用time.process_time()。例如:
import time from functools import wraps def timethis(func): @wraps(func) def wrapper(*args, **kwargs): start = time.process_time() r = func(*args, **kwargs) end = time.process_time() print("{}.{} : {}".format(func.__module__, func.__name__, end - start)) return r return wrapper
最后也是相當(dāng)重要的就是,如果你想做一個(gè)詳細(xì)的性能評估的話,你最好查閱time,timeit以及其他相關(guān)模塊的文檔,這樣你才能夠?qū)ζ脚_相關(guān)的不同之處有所了解。
profile模塊中最基礎(chǔ)的東西就是run()函數(shù)了。該函數(shù)會(huì)把一個(gè)語句字符串作為參數(shù),然后在執(zhí)行語句時(shí)生成所花費(fèi)的時(shí)間報(bào)告。
import profile def fib(n): # from literateprograms.org # http://bit.ly/hlOQ5m if n == 0: return 0 elif n == 1: return 1 else: return fib(n-1) + fib(n-2) def fib_seq(n): seq = [] if n > 0: seq.extend(fib_seq(n-1)) seq.append(fib(n)) return seq profile.run("print(fib_seq(20)); print")性能優(yōu)化
當(dāng)你的程序運(yùn)行地很慢的時(shí)候,你就會(huì)想去提升它的運(yùn)行速度,但是你又不想去借用一些復(fù)雜方案的幫助,比如使用C擴(kuò)展或是just-in-time(JIT)編譯器。
那么這時(shí)候應(yīng)該怎么辦呢?要牢記性能優(yōu)化的第一要義就是“不要為了優(yōu)化而去優(yōu)化,應(yīng)該在我們開始寫代碼之前就想好應(yīng)該怎樣編寫高性能的代碼”。第二要義就是“優(yōu)化一定要抓住重點(diǎn),找到程序中最重要的地方去優(yōu)化,而不要去優(yōu)化那些不重要的部分”。
通常來講,你會(huì)發(fā)現(xiàn)你的程序在某些熱點(diǎn)上花費(fèi)了很多時(shí)間,比如內(nèi)部數(shù)據(jù)的循環(huán)處理。一旦你發(fā)現(xiàn)了問題所在,你就可以對癥下藥,讓你的程序更快地執(zhí)行。
使用函數(shù)許多開發(fā)者剛開始的時(shí)候會(huì)將Python作為一個(gè)編寫簡單腳本的工具。當(dāng)編寫腳本的時(shí)候,很容易就會(huì)寫一些沒有結(jié)構(gòu)的代碼出來。例如:
import sys import csv with open(sys.argv[1]) as f: for row in csv.reader(f): # Some kind of processing
但是,卻很少有人知道,定義在全局范圍內(nèi)的代碼要比定義在函數(shù)中的代碼執(zhí)行地慢。他們之間速度的差別是因?yàn)榫植孔兞颗c全局變量不同的實(shí)現(xiàn)所引起的(局部變量的操作要比全局變量來得快)。所以,如果你想要讓程序更快地運(yùn)行,那么你可以簡單地將代碼放在一個(gè)函數(shù)中,就像這樣:
import sys import csv def main(filename): with open(filename) as f: for row in csv.reader(f): # Some kind of processing ... main(sys.argv[1])
這樣操作以后,處理速度會(huì)有提升,但是這個(gè)提升的程度依賴于程序的復(fù)雜性。根據(jù)經(jīng)驗(yàn)來講,通常都會(huì)提升15%到30%之間。
選擇性地減少屬性的訪問當(dāng)使用點(diǎn)(.)操作符去訪問屬性時(shí)都會(huì)帶來一定的消耗。本質(zhì)上來講,這會(huì)觸發(fā)一些特殊方法的執(zhí)行,比如__getattribute__()和__getattr__(),這通常都會(huì)導(dǎo)致去內(nèi)存中字典數(shù)據(jù)的查詢。
你可以通過兩種方式來避免屬性的訪問,第一種是使用from module import name的方式。第二種是將對象的方法名保存下來,在調(diào)用時(shí)直接使用。為了解釋地更加清楚,我們來看一個(gè)例子:
import math def compute_roots(nums): result = [] for n in nums: result.append(math.sqrt(n)) return result # Test nums = range(1000000) for n in range(100): r = compute_roots(nums)
上面的代碼在我的計(jì)算機(jī)上運(yùn)行大概需要40秒的時(shí)間。現(xiàn)在我們把上面代碼中的compute_roots()函數(shù)改寫一下:
from math import sqrt def compute_roots(nums): result = [] result_append = result.append for n in nums: result_append(sqrt(n)) return result nums = range(1000000) for n in range(100): r = compute_roots(nums)
這個(gè)版本的代碼執(zhí)行一下大概需要29秒。這兩個(gè)版本的代碼唯一的不同之處在于后面一個(gè)版本減少了對屬性的訪問。在后面一段代碼中,我們使用了sqrt()方法,而非math.sqrt()。result.append()函數(shù)也被存進(jìn)了一個(gè)局部變量result_append中,然后在循環(huán)當(dāng)中重復(fù)使用。
然而,有必要強(qiáng)調(diào)一點(diǎn)是說,這種方式的優(yōu)化僅僅針對經(jīng)常運(yùn)行的代碼有效,比如循環(huán)。由此可見,優(yōu)化僅僅在那些小心挑選出來的地方才會(huì)真正得到體現(xiàn)。
理解變量的局部性上面已經(jīng)講過,局部變量的操作比全局變量來得快。對于經(jīng)常要訪問的變量來說,最好把他們保存成局部變量。例如,考慮剛才已經(jīng)討論過的compute_roots()函數(shù)修改版:
import math def compute_roots(nums): sqrt = math.sqrt result = [] result_append = result.append for n in nums: result_append(sqrt(n)) return result
在這個(gè)版本中,sqrt函數(shù)被一個(gè)局部變量所替代。如果你執(zhí)行這段代碼的話,大概需要25秒就執(zhí)行完了(前一個(gè)版本需要29秒)。 這次速度的提升是因?yàn)閟qrt局部變量的查詢比sqrt函數(shù)的全局查詢來得稍快。
局部性原來同樣適用于類的參數(shù)。通常來講,使用self.name要比直接訪問局部變量來得慢。在內(nèi)部循環(huán)中,我們可以將經(jīng)常要訪問的屬性保存為一個(gè)局部變量。例如:
#Slower class SomeClass: ... def method(self): for x in s: op(self.value) # Faster class SomeClass: ... def method(self): value = self.value for x in s: op(value)避免不必要的抽象
任何時(shí)候當(dāng)你想給你的代碼添加其他處理邏輯,比如添加裝飾器,屬性或是描述符,你都是在拖慢你的程序。例如,考慮這樣一個(gè)類:
class A: def __init__(self, x, y): self.x = x self.y = y @property def y(self): return self._y @y.setter def y(self, value): self._y = value
現(xiàn)在,讓我們簡單地測試一下:
>>> from timeit import timeit >>> a = A(1,2) >>> timeit("a.x", "from __main__ import a") 0.07817923510447145 >>> timeit("a.y", "from __main__ import a") 0.35766440676525235 >>>
正如你所看到的,我們訪問屬性y比訪問簡單屬性x不是慢了一點(diǎn)點(diǎn),整整慢了4.5倍之多。如果你在乎性能的話,你就很有必要問一下你自己,對y的那些額外的定義是否都是必要的了。如果不是的話,那么你應(yīng)該把那些額外的定義刪掉,用一個(gè)簡單的屬性就夠了。如果只是因?yàn)樵谄渌Z言里面經(jīng)常使用getter和setter函數(shù)的話,你完全沒有必要在Python中也使用相同的編碼風(fēng)格。
使用內(nèi)置的容器內(nèi)置的數(shù)據(jù)結(jié)構(gòu),例如字符串(string),元組(tuple),列表(list),集合(set)以及字典(dict)都是用C語言實(shí)現(xiàn)的,正是因?yàn)椴捎昧薈來實(shí)現(xiàn),所以它們的性能表現(xiàn)也很好。如果你傾向于使用你自己的數(shù)據(jù)結(jié)構(gòu)作為替代的話(例如,鏈表,平衡樹或是其他數(shù)據(jù)結(jié)構(gòu)),想達(dá)到內(nèi)置數(shù)據(jù)結(jié)構(gòu)的速度的話是非常困難的。因此,你應(yīng)該盡可能地使用內(nèi)置的數(shù)據(jù)結(jié)構(gòu)。
避免不必要的數(shù)據(jù)結(jié)構(gòu)或是數(shù)據(jù)拷貝有時(shí)候程序員會(huì)有點(diǎn)兒走神,在不該用到數(shù)據(jù)結(jié)構(gòu)的地方去用數(shù)據(jù)結(jié)構(gòu)。例如,有人可能會(huì)寫這樣的的代碼:
values = [x for x in sequence] squares = [x*x for x in values]
也許他這么寫是為了先得到一個(gè)列表,然后再在這個(gè)列表上進(jìn)行一些操作。但是第一個(gè)列表是完全沒有必要寫在這里的。我們可以簡單地把代碼寫成這樣就行了:
squares = [x*x for x in sequence]
有鑒于此,你要小心那些偏執(zhí)程序員所寫的代碼了,這些程序員對Python的值共享機(jī)制非常偏執(zhí)。函數(shù)copy.deepcopy()的濫用也許是一個(gè)信號,表明該代碼是由菜鳥或者是不相信Python內(nèi)存模型的人所編寫的。在這樣的代碼里,減少copy的使用也許會(huì)比較安全。
在優(yōu)化之前,很有必要先詳細(xì)了解一下你所要使用的算法。如果你能夠?qū)⑺惴ǖ膹?fù)雜度從O(n^2)降為O(n log n)的話,程序的性能將得到極大的提高。
如果你已經(jīng)打算進(jìn)行優(yōu)化工作了,那就很有必要全局地考慮一下。普適的原則就是,不要想去優(yōu)化程序的每一個(gè)部分,這是因?yàn)閮?yōu)化工作會(huì)讓代碼變得晦澀難懂。相反,你應(yīng)該把注意力集中在已知的性能瓶頸處,例如內(nèi)部循環(huán)。
你需要謹(jǐn)慎地對待微優(yōu)化(micro-optimization)的結(jié)果。例如,考慮下面兩種創(chuàng)建字典結(jié)構(gòu)的方式:
a = { "name" : "AAPL", "shares" : 100, "price" : 534.22 } b = dict(name="AAPL", shares=100, price=534.22)
后面那一種方式打字打的更少一些(因?yàn)槟悴槐貙ey的名字用雙引號括起來)。然而當(dāng)你將這兩種編碼方式進(jìn)行性能對比時(shí),你會(huì)發(fā)現(xiàn)使用dict()函數(shù)的方式比另一種慢了3倍之多!知道了這一點(diǎn)以后,你也許會(huì)傾向于掃描你的代碼,把任何出現(xiàn)dict()的地方替換為另一種冗余的寫法。然而,一個(gè)聰明的程序員絕對不會(huì)這么做,他只會(huì)將注意力放在值得關(guān)注的地方,比如在循環(huán)上。在其他地方,速度的差異并不是最重要的。但是,如果你想讓你的程序性能有質(zhì)的飛躍的話,你可以去研究下基于JIT技術(shù)的工具。比如,PyPy項(xiàng)目,該項(xiàng)目是Python解釋器的另一種實(shí)現(xiàn),它能夠分析程序的執(zhí)行并為經(jīng)常執(zhí)行的代碼生成機(jī)器碼,有時(shí)它甚至能夠讓Python程序的速度提升一個(gè)數(shù)量級,達(dá)到(甚至超過)C語言編寫的代碼的速度。但是不幸的是,在本文正在寫的時(shí)候,PyPy還沒有完全支持Python 3。所以,我們還是在將來再來看它到底會(huì)發(fā)展的怎么樣。基于JIT技術(shù)的還有Numba項(xiàng)目。該項(xiàng)目實(shí)現(xiàn)的是一個(gè)動(dòng)態(tài)的編譯器,你可以將你想要優(yōu)化的Python函數(shù)以注解的方式進(jìn)行標(biāo)記,然后這些代碼就會(huì)在LLVM的幫助下被編譯成機(jī)器碼。該項(xiàng)目也能夠帶來極大的性能上的提升。然而,就像PyPy一樣,該項(xiàng)目對Python 3的支持還只是實(shí)驗(yàn)性的。
最后,但是也很重要的是,請牢記John Ousterhout(譯者注:Tcl和Tk的發(fā)明者,現(xiàn)為斯坦福大學(xué)計(jì)算機(jī)系的教授)說過的話“將不工作的東西變成能夠工作的,這才是最大的性能提升”。在你需要優(yōu)化前不要過分地考慮程序的優(yōu)化工作。程序的正確性通常來講都比程序的性能要來的重要。
--
原文:Developer Tools in Python
轉(zhuǎn)載自:伯樂在線 - brightconan
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/8678.html
摘要:發(fā)布于之后,采用了完全不同的方式,使用函數(shù)定義任務(wù)。它允許開發(fā)者使用它們的補(bǔ)丁和更新來修復(fù)這些安全漏洞。提供了工具用于掃描依賴來監(jiān)測漏洞。是一個(gè)開源診斷工具,用于和應(yīng)用。是和開發(fā)的一款新的包管理工具。與相比,它解決了安全性能以及一致性問題。 譯者按: 最全的JavaScript開發(fā)工具列表,總有一款適合你! 原文: THE ULTIMATE LIST OF JAVASCRIPT TOO...
摘要:特意對前端學(xué)習(xí)資源做一個(gè)匯總,方便自己學(xué)習(xí)查閱參考,和好友們共同進(jìn)步。 特意對前端學(xué)習(xí)資源做一個(gè)匯總,方便自己學(xué)習(xí)查閱參考,和好友們共同進(jìn)步。 本以為自己收藏的站點(diǎn)多,可以很快搞定,沒想到一入?yún)R總深似海。還有很多不足&遺漏的地方,歡迎補(bǔ)充。有錯(cuò)誤的地方,還請斧正... 托管: welcome to git,歡迎交流,感謝star 有好友反應(yīng)和斧正,會(huì)及時(shí)更新,平時(shí)業(yè)務(wù)工作時(shí)也會(huì)不定期更...
摘要:建模語言建模語言是可用于表達(dá)信息或知識或系統(tǒng)的任何人造語言,該結(jié)構(gòu)由一組一致的規(guī)則定義,目標(biāo)是可視化,推理,驗(yàn)證和傳達(dá)系統(tǒng)設(shè)計(jì)。將這些文件安排到不同的地方稱為源代碼樹。源代碼樹的結(jié)構(gòu)通常反映了軟件的體系結(jié)構(gòu)。 大綱 軟件構(gòu)建的一般過程: 編程/重構(gòu) 審查和靜態(tài)代碼分析 調(diào)試(傾倒和記錄)和測試 動(dòng)態(tài)代碼分析/分析 軟件構(gòu)建的狹義過程(Build): 構(gòu)建系統(tǒng):組件和過程 構(gòu)建變體...
摘要:讓你收獲滿滿碼個(gè)蛋從年月日推送第篇文章一年過去了已累積推文近篇文章,本文為年度精選,共計(jì)篇,按照類別整理便于讀者主題閱讀。本篇文章是今年的最后一篇技術(shù)文章,為了讓大家在家也能好好學(xué)習(xí),特此花了幾個(gè)小時(shí)整理了這些文章。 showImg(https://segmentfault.com/img/remote/1460000013241596); 讓你收獲滿滿! 碼個(gè)蛋從2017年02月20...
閱讀 1369·2021-10-19 11:42
閱讀 717·2021-09-22 16:04
閱讀 1867·2021-09-10 11:23
閱讀 1838·2021-07-29 14:48
閱讀 1247·2021-07-26 23:38
閱讀 2812·2019-08-30 15:54
閱讀 1024·2019-08-30 11:25
閱讀 1694·2019-08-29 17:23