Python 程序員必知必會的開發者工具

jopen 12年前發布 | 32K 次閱讀 Python 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&#039;t convert &#039;int&#039; 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(&#039;Hello&#039;)
> sample.py(4)func()
-> return n + 10
(Pdb) print n
&#039;Hello&#039;
(Pdb) q
>>>

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

import traceback
import sys
try:
    func(arg)
except:
    print(&#039;**** AN ERROR OCCURRED ****&#039;)
    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調試器入門的資源列表:

  1. 閱讀Steve Ferb的文章 &#8220;Debugging in Python&#8221;

  2. 觀看Eric Holscher的截圖 &#8220;Using pdb, the Python Debugger&#8221;

  3. 閱讀Ayman Hourieh的文章 &#8220;Python Debugging Techniques&#8221;

  4. 閱讀 Python documentation for pdb &#8211; The Python Debugger

  5. 閱讀Karen Tracey的D jango 1.1 Testing and Debugging一書中的第九章——When You Don&#8217;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(&#039;book.py&#039;))
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(&#039;{}.{} : {}&#039;.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(&#039;{} : {}&#039;.format(label, end - start))

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

>>> with timeblock(&#039;counting&#039;):
...     n = 10000000
...     while n > 0:
...         n -= 1
...
counting : 1.5551159381866455
>>>

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

>>> from timeit import timeit
>>> timeit(&#039;math.sqrt(2)&#039;, &#039;import math&#039;)
0.1432319980012835
>>> timeit(&#039;sqrt(2)&#039;, &#039;from math import sqrt&#039;)
0.10836604500218527
>>>

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

>>> timeit(&#039;math.sqrt(2)&#039;, &#039;import math&#039;, number=10000000)
1.434852126003534
>>> timeit(&#039;sqrt(2)&#039;, &#039;from math import sqrt&#039;, 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(&#039;{}.{} : {}&#039;.format(func.__module__, func.__name__, end - start))
        return r
    return wrapper

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

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(&#039;print(fib_seq(20)); print&#039;)

性能優化

當你的程序運行地很慢的時候,你就會想去提升它的運行速度,但是你又不想去借用一些復雜方案的幫助,比如使用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(&#039;a.x&#039;, &#039;from __main__ import a&#039;)
0.07817923510447145
>>> timeit(&#039;a.y&#039;, &#039;from __main__ import a&#039;)
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 = {
&#039;name&#039; : &#039;AAPL&#039;,
&#039;shares&#039; : 100,
&#039;price&#039; : 534.22
}
b = dict(name=&#039;AAPL&#039;, 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的發明者,現為斯坦福大學計算機系的教授)說過的話“將不工作的東西變成能夠工作的,這才是最大的性能提升”。在你需要優化前不要過分地考慮程序的優化工作。程序的正確性通常來講都比程序的性能要來的重要。

原文鏈接: PyPix.com   翻譯: 伯樂在線 - brightconan
譯文鏈接: http://blog.jobbole.com/58226/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!