Python 開發工具集:關於文檔、測試、調試、程序的優化和分析

Python已經演化出了一個普遍的生態系統,該生態系統可以讓Python程序員的生活變得更加簡單,減小他們重複造輪的工做。一樣的理念也適用於工具開發者的工做,即使他們開發出的工具並無出如今最終的程序中。本文將介紹Python程序員必知必會的開發者工具。html

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

trace模塊能夠監控Python執行程序的方式,同時生成一個報表來顯示程序的每一行執行的次數。這些信息能夠用來發現未被自動化測試集所覆蓋的程序執行路徑,也能夠用來研究程序調用圖,進而發現模塊之間的依賴關係。編寫並執行測試能夠發現絕大多數程序中的問題,Python使得debug工做變得更加簡單,這是由於在大部分狀況下,Python都可以將未被處理的錯誤打印到控制檯中,咱們稱這些錯誤信息爲traceback。若是程序不是在文本控制檯中運行的,traceback也可以將錯誤信息輸出到日誌文件或是消息對話框中。當標準的traceback沒法提供足夠的信息時,可使用cgitb 模塊來查看各級棧和源代碼上下文中的詳細信息,好比局部變量。cgitb模塊還可以將這些跟蹤信息以HTML的形式輸出,用來報告web應用中的錯誤。git

一旦發現了問題出在哪裏後,就須要使用到交互式調試器進入到代碼中進行調試工做了,pdb模塊可以很好地勝任這項工做。該模塊能夠顯示出程序在錯誤產生時的執行路徑,同時能夠動態地調整對象和代碼進行調試。當程序經過測試並調試後,下一步就是要將注意力放到性能上了。開發者可使用profile以及 timit 模塊來測試程序的速度,找出程序中究竟是哪裏很慢,進而對這部分代碼獨立出來進行調優的工做。Python程序是經過解釋器執行的,解釋器的輸入是原有程序的字節碼編譯版本。這個字節碼編譯版本能夠在程序執行時動態地生成,也能夠在程序打包的時候就生成。compileall 模塊能夠處理程序打包的事宜,它暴露出了打包相關的接口,該接口可以被安裝程序和打包工具用來生成包含模塊字節碼的文件。同時,在開發環境中,compileall模塊也能夠用來驗證源文件是否包含了語法錯誤。程序員

在源代碼級別,pyclbr 模塊提供了一個類查看器,方便文本編輯器或是其餘程序對Python程序中有意思的字符進行掃描,好比函數或者是類。在提供了類查看器之後,就無需引入代碼,這樣就避免了潛在的反作用影響。web

文檔字符串與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文件用來測試,以下所示:shell

# 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).django

若是不想新建一個單獨的測試文件的話,那麼另外一種選擇就是在文件末尾包含相應的測試代碼:編程

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調試器入門的資源列表:

程序分析

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
>>>

若是想研究一小段代碼的性能的話,timeit模塊會很是有用。例如:

>>> from timeit import timeit
>>> timeit('math.sqrt(2)', 'import math')
0.1432319980012835
>>> timeit('sqrt(2)', 'from math import sqrt')
0.10836604500218527
>>>

timeit的工做原理是,將第一個參數中的語句執行100萬次,而後計算所花費的時間。第二個參數指定了一些測試以前須要作的環境準備工做。若是你須要改變迭代的次數,能夠附加一個number參數,就像這樣:

>>> timeit('math.sqrt(2)', 'import math', number=10000000)
1.434852126003534
>>> timeit('sqrt(2)', 'from math import sqrt', number=10000000)
1.0270336690009572
>>>

當進行性能評估的時候,要牢記任何得出的結果只是一個估算值。函數time.perf_counter()可以在任一平臺提供最高精度的計時器。然而,它也只是記錄了天然時間,記錄天然時間會被不少其餘因素影響,好比,計算機的負載。若是你對處理時間而非天然時間感興趣的話,你可使用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

最後也是至關重要的就是,若是你想作一個詳細的性能評估的話,你最好查閱time,timeit以及其餘相關模塊的文檔,這樣你纔可以對平臺相關的不一樣之處有所瞭解。

profile模塊中最基礎的東西就是run()函數了。該函數會把一個語句字符串做爲參數,而後在執行語句時生成所花費的時間報告。

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')

性能優化

當你的程序運行地很慢的時候,你就會想去提高它的運行速度,可是你又不想去借用一些複雜方案的幫助,好比使用C擴展或是just-in-time(JIT)編譯器。

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

一般來說,你會發現你的程序在某些熱點上花費了不少時間,好比內部數據的循環處理。一旦你發現了問題所在,你就能夠對症下藥,讓你的程序更快地執行。

使用函數

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

import sys
import csv
with open(sys.argv[1]) as f:
    for row in csv.reader(f):
    # Some kind of processing

可是,卻不多有人知道,定義在全局範圍內的代碼要比定義在函數中的代碼執行地慢。他們之間速度的差異是由於局部變量與全局變量不一樣的實現所引發的(局部變量的操做要比全局變量來得快)。因此,若是你想要讓程序更快地運行,那麼你能夠簡單地將代碼放在一個函數中,就像這樣:

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])

這樣操做之後,處理速度會有提高,可是這個提高的程度依賴於程序的複雜性。根據經驗來說,一般都會提高15%到30%之間。

選擇性地減小屬性的訪問

當使用點(.)操做符去訪問屬性時都會帶來必定的消耗。本質上來說,這會觸發一些特殊方法的執行,好比__getattribute__()__getattr__(),這一般都會致使去內存中字典數據的查詢。

你能夠經過兩種方式來避免屬性的訪問,第一種是使用from module import name的方式。第二種是將對象的方法名保存下來,在調用時直接使用。爲了解釋地更加清楚,咱們來看一個例子:

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)

上面的代碼在個人計算機上運行大概須要40秒的時間。如今咱們把上面代碼中的compute_roots()函數改寫一下:

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)

這個版本的代碼執行一下大概須要29秒。這兩個版本的代碼惟一的不一樣之處在於後面一個版本減小了對屬性的訪問。在後面一段代碼中,咱們使用了sqrt()方法,而非math.sqrt()。result.append()函數也被存進了一個局部變量result_append中,而後在循環當中重複使用。

然而,有必要強調一點是說,這種方式的優化僅僅針對常常運行的代碼有效,好比循環。因而可知,優化僅僅在那些當心挑選出來的地方纔會真正獲得體現。

理解變量的局部性

上面已經講過,局部變量的操做比全局變量來得快。對於常常要訪問的變量來講,最好把他們保存成局部變量。例如,考慮剛纔已經討論過的compute_roots()函數修改版:

import math

def compute_roots(nums):
    sqrt = math.sqrt
    result = []
    result_append = result.append
    for n in nums:
        result_append(sqrt(n))
    return result

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

局部性原來一樣適用於類的參數。一般來說,使用self.name要比直接訪問局部變量來得慢。在內部循環中,咱們能夠將常常要訪問的屬性保存爲一個局部變量。例如:

#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)

避免沒必要要的抽象

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

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

如今,讓咱們簡單地測試一下:

>>> 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不是慢了一點點,整整慢了4.5倍之多。若是你在意性能的話,你就頗有必要問一下你本身,對y的那些額外的定義是否都是必要的了。若是不是的話,那麼你應該把那些額外的定義刪掉,用一個簡單的屬性就夠了。若是隻是由於在其餘語言裏面常用getter和setter函數的話,你徹底沒有必要在Python中也使用相同的編碼風格。

使用內置的容器

內置的數據結構,例如字符串(string),元組(tuple),列表(list),集合(set)以及字典(dict)都是用C語言實現的,正是由於採用了C來實現,因此它們的性能表現也很好。若是你傾向於使用你本身的數據結構做爲替代的話(例如,鏈表,平衡樹或是其餘數據結構),想達到內置數據結構的速度的話是很是困難的。所以,你應該儘量地使用內置的數據結構。

避免沒必要要的數據結構或是數據拷貝

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

values = [x for x in sequence]
squares = [x*x for x in values]

也許他這麼寫是爲了先獲得一個列表,而後再在這個列表上進行一些操做。可是第一個列表是徹底沒有必要寫在這裏的。咱們能夠簡單地把代碼寫成這樣就好了:

squares = [x*x for x in sequence]

有鑑於此,你要當心那些偏執程序員所寫的代碼了,這些程序員對Python的值共享機制很是偏執。函數copy.deepcopy()的濫用也許是一個信號,代表該代碼是由菜鳥或者是不相信Python內存模型的人所編寫的。在這樣的代碼裏,減小copy的使用也許會比較安全。

在優化以前,頗有必要先詳細瞭解一下你所要使用的算法。若是你可以將算法的複雜度從O(n^2)降爲O(n log n)的話,程序的性能將獲得極大的提升。

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

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

a = {
'name' : 'AAPL',
'shares' : 100,
'price' : 534.22
}
b = dict(name='AAPL', shares=100, price=534.22)

後面那一種方式打字打的更少一些(由於你沒必要將key的名字用雙引號括起來)。然而當你將這兩種編碼方式進行性能對比時,你會發現使用dict()函數的方式比另外一種慢了3倍之多!知道了這一點之後,你也許會傾向於掃描你的代碼,把任何出現dict()的地方替換爲另外一種冗餘的寫法。然而,一個聰明的程序員絕對不會這麼作,他只會將注意力放在值得關注的地方,好比在循環上。在其餘地方,速度的差別並非最重要的。可是,若是你想讓你的程序性能有質的飛躍的話,你能夠去研究下基於JIT技術的工具。好比,PyPy項目,該項目是Python解釋器的另外一種實現,它可以分析程序的執行併爲常常執行的代碼生成機器碼,有時它甚至可以讓Python程序的速度提高一個數量級,達到(甚至超過)C語言編寫的代碼的速度。可是不幸的是,在本文正在寫的時候,PyPy尚未徹底支持Python 3。因此,咱們仍是在未來再來看它到底會發展的怎麼樣。基於JIT技術的還有Numba項目。該項目實現的是一個動態的編譯器,你能夠將你想要優化的Python函數以註解的方式進行標記,而後這些代碼就會在LLVM的幫助下被編譯成機器碼。該項目也可以帶來極大的性能上的提高。然而,就像PyPy同樣,該項目對Python 3的支持還只是實驗性的。

最後,可是也很重要的是,請牢記John Ousterhout(譯者注:Tcl和Tk的發明者,現爲斯坦福大學計算機系的教授)說過的話「將不工做的東西變成可以工做的,這纔是最大的性能提高」。在你須要優化前不要過度地考慮程序的優化工做。程序的正確性一般來說都比程序的性能要來的重要。

--

原文:Developer Tools in Python
轉載自:伯樂在線 - brightconan

相關文章
相關標籤/搜索