Python編程規范及性能優化
Python編程規范及性能優化
Ptyhon編程規范
編碼
所有的 Python 腳本文件都應在文件頭標上 # -*- coding:utf-8 -*- 。設置編輯器,默認保存為 utf-8 格式。
注釋
業界普遍認同 Python 的注釋分為兩種的概念,一種是由 # 開頭的“真正的”注釋,另一種是 docstrings。前者表明為何選擇當前實現以及這種實現的原理和難點,后者表明如何使用這個包、模塊、類、函數(方法),甚至包括使用示例和單元測試。
堅持適當注釋原則。對不存在技術難點的代碼堅持不注釋,對存在技術難點的代碼必須注釋。但與注釋不同,推薦對每一個包、模塊、類、函數(方法)寫 docstrings,除非代碼一目了然,非常簡單。
縮進
Python 依賴縮進來確定代碼塊的層次,行首空白符主要有兩種:tab 和空格,但嚴禁兩者混用。如果使用 tab 縮進,設定 tab 為 4 個空格。
公司內部推薦使用 4 個空格的 tab 進行縮進。
空格
空格在 Python 代碼中是有意義的,因為 Python 的語法依賴于縮進,在行首的空格稱為前導空格。在這一節不討論前導空格相關的內容,只討論非前導空格。非前導空格在 Python 代碼中沒有意義,但適當地加入非前導空格可以增進代碼的可讀性。
1)在二元算術、邏輯運算符前后加空格:如 a = b + c;
2)在一元前綴運算符后不加空格,如 if !flg: pass;
3)“:”用在行尾時前后皆不加空格,如分枝、循環、函數和類定義語言;用在非行尾時兩端加空格,如 dict 對象的定義 d = {‘key’ : ’value’}。
4)括號(含圓括號、方括號和花括號)前后不加空格,如 do_something(arg1, arg2),而不是 do_something( arg1, arg2 );
5)逗號后面加一個空格,前面不加空格;
空行
適當的空行有利于增加代碼的可讀性,加空行可以參考如下幾個準則:
1)在類、函數的定義間加空行;
2)在 import 不同種類的模塊間加空行;
3)在函數中的邏輯段落間加空行,即把相關的代碼緊湊寫在一起,作為一個邏輯段落,段落間以空行分隔;
命名
一致的命名可以給開發人員減少許多麻煩,而恰如其分的命名則可以大幅提高代碼的可讀性,降低維護成本。
常量
常量名所有字母大寫,由下劃線連接各個單詞,如
WHITE = 0XFFFFFF
THIS_IS_A_CONSTANT = 1
變量
變量名全部小寫,由下劃線連接各個單詞,如
color = WHITE
this_is_a_variable = 1
不論是類成員變量還是全局變量,均不使用 m 或 g 前綴。私有類成員使用單一下劃線前綴標識,多定義公開成員,少定義私有成員。
變量名不應帶有類型信息,因為 Python 是動態類型語言。如iValue、names_list、dict_obj 等都是不好的命名。
函數
函數名的命名規則與變量名相同。
類
類名單詞首字母大寫,不使用下劃線連接單詞,也不加入 C、T 等前綴。如:
class ThisIsAClass(object):
pass
模塊
模塊名全部小寫,對于包內使用的模塊,可以加一個下劃線前綴,如:
module.py
_internal_module.py
包
包的命名規范與模塊相同。
縮寫
命名應當盡量使用全拼寫的單詞,縮寫的情況有如下兩種:
1)常用的縮寫,如 XML、ID等,在命名時也應只大寫首字母,如:
class XmlParser(object):pass
2)命名中含有長單詞,對某個單詞進行縮寫。這時應使用約定成俗的縮寫方式,如去除元音、包含輔音的首字符等方式,例如:
function 縮寫為 fn
text 縮寫為 txt
object 縮寫為 obj
count 縮寫為 cnt
number 縮寫為 num,等。
特定命名方式
主要是指 __xxx__ 形式的系統保留字命名法。項目中也可以使用這種命名,它的意義在于這種形式的變量是只讀的,這種形式的類成員函數盡量不要重載。如:
class Base(object):
def __init__(self, id, parent = None):
self.__id__ = id
self.__parent__ = parent
def __message__(self, msgid):
# …略
其中 __id__、__parent__ 和 __message__ 都采用了系統保留字命名法。
語句
Import
import 語句有以下幾個原則需要遵守:
1)import 的次序,先 import Python 內置模塊,再 import 第三方模塊,最后 import 自己開發的項目中的其它模塊;這幾種模塊中用空行分隔開來。
2)一條 import 語句 import 一個模塊。
3)當從模塊中 import 多個對象且超過一行時,使用如下斷行法(此語法 py2.5 以上版本才支持):
from module import (obj1, obj2, obj3, obj4,obj5, obj6)
4)不要使用 from module import *,除非是 import 常量定義模塊或其它你確保不會出現命名空間沖突的模塊。
賦值
對于賦值語言,主要是不要做無謂的對齊,如:
a = 1 # 這是一個行注釋
variable = 2 # 另一個行注釋
fn = callback_function # 還是行注釋
沒有必要做這種對齊,原因有兩點:一是這種對齊會打亂編程時的注意力,大腦要同時處理兩件事(編程和對齊);二是以后閱讀和維護都很困難,因為人眼的橫向視野很窄,把三個字段看成一行很困難,而且維護時要增加一個更長的變量名也會破壞對齊。直接這樣寫為佳:
a = 1 # 這是一個行注釋
variable = 2 # 另一個行注釋
fn = callback_function # 還是行注釋
分支和循環
對于分枝和循環,有如下幾點需要注意的:
1)不要寫成一行,如:
if !flg: pass 和 for i in xrange(10): print i都不是好代碼,應寫成
if !flg:
pass
for i in xrange(10):
print i
注:本文檔中出現寫成一行的例子是因為排版的原因,不得作為編碼中不斷行的依據。
2)條件表達式的編寫應該足夠 pythonic,如以下形式的條件表達式是拙劣的:
if len(alist) != 0: do_something()
if alist != []: do_something()
if s != “”: do_something()
if var != None: do_something()
if var != False: do_something()
上面的語句應該寫成:
if seq: do_somethin() # 注意,這里命名也更改了
if var: do_something()
3)用得著的時候多使用循環語句的 else 分句,以簡化代碼。
已有代碼
對于項目中已有的代碼,可能因為歷史遺留原因不符合本規范,應當看作可以容忍的特例,允許存在;但不應在新的代碼中延續舊的風格。
對于第三方模塊,可能不符合本規范,也應看作可以容忍的特例,允許存在;但不應在新的代碼中使用第三方模塊的風格。
tab 與空格混用的縮進是不可容忍的,在運行項目時應使用 –t 或 –tt 選項排查這種可能性存在。出現混用的情況時,如果是公司開發的基礎類庫代碼,應當通知類庫維護人員修改;第三方模塊則可以通過提交 patch 等方式敦促開發者修正問題。
已有風格
開發人員往往在加入項目之前已經形成自有的編碼風格,加入項目后應以本規范為準編寫代碼。特別是匈牙利命名法,因為帶有類型信息,并不適合 Python 編程,不應在 Python 項目中應用。
Python性能優化
閱讀 Zen of Python,在Python解析器中輸入 import this. 一個犀利的Python新手可能會注意到"解析"一詞, 認為Python不過是另一門腳本語言. "它肯定很慢!"
毫無疑問:Python程序沒有編譯型語言高效快速. 甚至Python擁護者們會告訴你Python不適合這些領域. 然而,油Tube已用Python服務于每小時4千萬視頻的請求. 你所要做的就是編寫高效的代碼和需要時使用外部實現(C/C++)代碼或外部第三方工具.
代碼優化
代碼優化能夠讓程序運行更快,它是在不改變程序運行結果的情況下使得程序的運行效率更高,根據 80/20 原則,實現程序的重構、優化、擴展以及文檔相關的事情通常需要消耗 80% 的工作量。優化通常包含兩方面的內容:減小代碼的體積,提高代碼的運行效率。
有很多方法可以用來縮短程序的執和時間.記住,每當執行一個Python腳本之時,就會調用一個解釋器.因而為了對此做補償,需要對代碼做點工作.這與Python是一種解釋性語言有很大關系,不過通過減少需加以分析的語句的數量,也會減少解釋器的總開銷.
順便提及,Python解釋器具有一個命令么選項(-0代表optimize--優化),使得程序以不執行某些字節操作的方式加以執行.一般來說, 該選項用于去除節字碼中給出異常產生的行號的注釋,并不編譯doc字符串和一些其他東西.該標志不會給出太多的速度增益,而且它會使用程序難以調試.
變量
取決于如何定義,解釋器花費或多或少的時間嘗試計算出它們的值.Python在嘗試判定變量名時利用動態作用域規則進行處理.當它在代碼中找到一個 變量時,首先通過查看局部名空間字典考察該變量是不是一個局部變量.如果找到該變量,就抓取該變量的值.否則再在全局名字空間字典中進行搜索,如果需要, 還會搜索內置名字空間.因此,局部變量比其他類型變量的搜索速度要快得多,因而獲得它們的值也要快得多.局部變量搜索速度快是因為它們對應于數組中的下標 操作,而全局變量搜索則對應于散列表搜索.一個良好的優化方法是:如果在函數中使用了很多全局變量,把它們的值賦給局部變量可能會有很大幫助.
模塊
在一個腳本之中,只需一次導入一個外部模塊即可.因此,在代碼中不需要多個import語句.實際上,應該避免在程序中嘗試再導入模塊.根據以往的 經驗, 應該把所有的import語句放在程序頭的最開始部分.然而,對一個模塊多次調用import不會真正造成問題,因為它只是一個字典查找.如果必須要對一 個外部模塊的某些特定屬性進行大量引用,開始編寫代碼之前,應該考慮將這些元素復制到單個變量中(當然,如果可能的話)--特另是如果引用在一個循環內部 進行.只要導入模塊,解釋器就查找該模塊的字節編譯版.如果未找到,它會自動對模塊進行字節編譯并生成.pyc文件.因此,當下次嘗試導入此模塊時,字節 編譯文件就在那里.正如讀者所體會的那樣.pyc文件比常規.py文件執行起來快很多,因為它們在執行之前就已經經解釋器解釋過.這里的建議是盡量使用字 節編譯模塊.不論是否擁有.pyc文件Python代碼都以相同的速度執行.惟一的區別是如果存在.pyc文件,啟動將會有所加快.代碼的實際運行速度沒 有區別.
字符串
python 中的字符串對象是不可改變的,因此對任何字符串的操作如拼接,修改等都將產生一個新的字符串對象,而不是基于原字符串,因此這種持續的 copy 會在一定程度上影響 python 的性能。對字符串的優化也是改善性能的一個重要的方面,特別是在處理文本較多的情況下。
1. 在字符串連接的使用盡量使用 join() 而不是 +;
2. 當對字符串可以使用正則表達式或者內置函數來處理的時候,選擇內置函數。如 str.isalpha(),str.isdigit(),str.startswith(('x', 'yz')),str.endswith(('x', 'yz'));
3. 對字符進行格式化比直接串聯讀取要快,因此要在字符串與其他變量連接時就使用格式化字符串.請查看下面的連接形式:
name="Andre"
print "Hello " + name
print "Hello %s" % name
顯然與第一個語句相比,第二個print語句更加優化.第三行中的括號是不需要的。
循環
對循環的優化所遵循的原則是盡量減少循環過程中的計算量,有多重循環的盡量將內層的計算提到上一層。
可以在循環中優化大量事件以便它們可平穩運行.下面就是可優化操作的簡短清單.在內循環中應該使用內置函數,而不是使用采用Python編寫的函 數.通過使用運行列表操作的內置函數(例如map(),reduce(),filter())代替直接循環,可以把一些循環開銷轉移到C代碼.向 map,reduce,filter傳送內置函數更會使性能得以提高.具有多重循環之時,只有最內層循環值得優化.優化多重循環時,旨在減少內存分配的次 數.使最內層循環成為交互作用次數最少者應該有助于性能設計.使用局部變量會大大改善循環內部的處理時間.只要可能,在進入循環前把所有全局變量和屬性搜 索復制到局部變量.如果在嵌套循環內部使用諸如range(n)之類的結構方法,則在最外層循環外部把值域分配到一個局部變量并在循環定義中使用該變量將 快速得多.
yRange=range(500) #優化1
for xItem in range(100000):
for yItem in yRange:
print xItem,yItem
這里的另一種優化是使用xrange作為循環的x,因為100000項列表是一個相當大的列表.
yRange=range(500)
for xItem in xRange(100000): #優化2
for yItem in yRange:
print xItem,yItem
函數
Python的內置函數比采用純Python語言編寫的函數執行速度要快,因為內置函數是采用C語言編寫的.map(),filter()以及 reduce就是在性能上優于采用Python編寫的函數的內置函數范例.還應了解,Python把函數名作為全局常數加以處理.既然如此,前面我們看到 的名字空間搜索的整個概念同樣適用于函數.如果可以選擇的話,使用map()函數的隱含循環代替for循環要快得多.我在這里提到的循環的執行時間在很大 程序上取決于傳送了什么函數.傳送Python函數沒有傳送內置函數(諸如在operator模塊里的那些函數)那么快.
在Python中函數調用代價還是很大的。在計算密集的地方,很大次數的循環體中要盡量減少函數的調用及調用層次(能inline最好inline)。
改進算法,選擇合適的數據結構
一個良好的算法能夠對性能起到關鍵作用,因此性能改進的首要點是對算法的改進。在算法的時間復雜度排序上依次是:
O(1) -> O(lg n) -> O(n lg n) -> O(n^2) -> O(n^3) -> O(n^k) -> O(k^n) -> O(n!)
因此如果能夠在時間復雜度上對算法進行一定的改進,對性能的提高不言而喻。
使用內建函數
你可以用Python寫出高效的代碼,但很難擊敗內建函數. 經查證. 他們非常快速.
使用join()連接字符串
你可以使用 "+" 來連接字符串. 但由于string在Python中是不可變的,每一個"+"操作都會創建一個新的字符串并復制舊內容. 常見用法是使用Python的數組模塊單個的修改字符;當完成的時候,使用 join() 函數創建最終字符串.
>>> #This is good to glue a large number of strings
>>> for chunk in input():
>>> my_string.join(chunk)
使用Python多重賦值,交換變量
在Python中即優雅又快速:
>>> x, y = y, x
這樣很慢:
>>> temp = x
>>> x = y
>>> y = temp
盡量使用局部變量
Python 檢索局部變量比檢索全局變量快. 這意味著,避免 "global" 關鍵字.
盡量使用 "in"
使用 "in" 關鍵字. 簡潔而快速.
>>> for key in sequence:
>>> print “found”
使用延遲加載加速
將 "import" 聲明移入函數中,僅在需要的時候導入. 換句話說,如果某些模塊不需馬上使用,稍后導入他們. 例如,你不必在一開使就導入大量模塊而加速程序啟動. 該技術不能提高整體性能. 但它可以幫助你更均衡的分配模塊的加載時間.
為無限循環使用 "while 1"
有時候在程序中你需一個無限循環.(例如一個監聽套接字的實例) 盡管 "while true" 能完成同樣的事, 但 "while 1" 是單步運算. 這招能提高你的Python性能.
使用 Lazy if-evaluation 的特性
Python 中條件表達式是 lazy evaluation 的,也就是說如果存在條件表達式 if x and y,在 x 為 false 的情況下 y 表達式的值將不再計算。因此可以利用該特性在一定程度上提高程序效率。
使用list comprehension和generator expression
從Python 2.0 開始,你可以使用 list comprehension 取代大量的 "for" 和 "while" 塊. 使用List comprehension通常更快,Python解析器能在循環中發現它是一個可預測的模式而被優化.額外好處是,list comprehension更具可讀性(函數式編程),并在大多數情況下,它可以節省一個額外的計數變量。列表解析要比在循環中重新構建一個新的 list 更為高效,因此我們可以利用這一特性來提高運行的效率。例如,讓我們計算1到10之間的偶數個數:
>>> # the good way to iterate a range
>>> evens = [ i for i in range(10) if i%2 == 0]
>>> [0, 2, 4, 6, 8]
>>> # the following is not so Pythonic
>>> i = 0
>>> evens = []
>>> while i < 10:
>>> if i %2 == 0: evens.append(i)
>>> i += 1
>>> [0, 2, 4, 6, 8]
生成器表達式則是在 2.4 中引入的新內容,語法和列表解析類似,但是在大數據量處理時,生成器表達式的優勢較為明顯,它并不創建一個列表,只是返回一個生成器,因此效率較高。例 如:代碼 a = [w for w in list] 修改為 a = (w for w in list),運行時間進一步減少,縮短約為 2.98s。
使用Dictionary comprehensions/Set comprehensions
大多數的Python程序員都知道且使用過列表推導(list comprehensions)。如果你對list comprehensions概念不是很熟悉——一個list comprehension就是一個更簡短、簡潔的創建一個list的方法。
>>> some_list = [1, 2, 3, 4, 5]
>>> another_list = [x + 1 for x in some_list]
>>> another_list
>>> [2, 3, 4, 5, 6]
自從python 3.1 (甚至是Python 2.7)起,我們可以用同樣的語法來創建集合和字典表:
>>>#Se Comprehensions
>>> some_list = [1, 2, 3, 4, 5, 2, 5, 1, 4, 8]
>>> even_set = { x for x in some_list if x % 2 == 0 }
>>> even_set set([8, 2, 4])
>>> # Dict Comprehensions
>>> d = {x: x % 2 == 0 for x in range(1, 11)}
>>> d {1: False, 2: True, 3: False, 4: True, 5: False, 6: True, 7: False, 8: True, 9: False, 10: True}
在第一個例子里,我們以some_list為基礎,創建了一個具有不重復元素的集合,而且集合里只包含偶數。而在字典表的例子里,我們創建了一個key是不重復的1到10之間的整數,value是布爾型,用來指示key是否是偶數。
這里另外一個值得注意的事情是集合的字面量表示法。我們可以簡單的用這種方法創建一個集合:
>>> my_set ={1, 2, 1, 2, 3, 4}
>>> my_set
set([1, 2, 3, 4])
而不需要使用內置函數set()。
集合 (set) 與列表 (list)
set 的 union, intersection,difference 操作要比 list 的迭代要快。因此如果涉及到求 list 交集,并集或者差的問題可以轉換為 set 來操作。
set(list1) | set(list2) |
union |
包含list1和list2所有數據的新集合 |
set(list1) & set(list2) |
intersection |
包含list1和list2中共同元素的新集合 |
set(list1) - set(list2) |
difference |
在list1中出現但不在list2中出現的元素的集合 |
使用dict 和 set 測試成員
Python dict中使用了 hash table,因此查找操作的復雜度為 O(1),因此對成員的查找訪問等操作字典要比 list 更快。
檢查一個元素是在dicitonary或set是否存在,這在Python中非常快的。這是因為dict和set使用哈希表來實現,查找效率可以達 到O(1),而 list 實際是個數組,在 list 中,查找需要遍歷整個 list,其復雜度為 O(n),因此,如果您需要經常檢查成員,使用 set 或 dict做為你的容器。
>>> mylist = ['a', 'b', 'c'] #Slower, check membership with list:
>>> ‘c’ in mylist
>>> True
>>> myset = set(['a', 'b', 'c']) # Faster, check membership with set:
>>> ‘c’ in myset:
>>> True
計數時使用Counter計數對象
這聽起來顯而易見,但經常被人忘記。對于大多數程序員來說,數一個東西是一項很常見的任務,而且在大多數情況下并不是很有挑戰性的事情——這里有幾種方法能更簡單的完成這種任務。
Python的collections類庫里有個內置的dict類的子類,是專門來干這種事情的:
>>>from collections import Counter
>>>c = Counter('hello world')
>>>c
Counter({'l': 3, 'o': 2, ' ': 1, 'e': 1, 'd': 1, 'h': 1, 'r': 1, 'w': 1})
>>>c.most_common(2)
[('l', 3), ('o', 2)]
使用xrange()處理長序列
這樣可為你節省大量的系統內存,因為xrange()在序列中每次調用只產生一個整數元素。而相反 range(),它將直接給你一個完整的元素列表,用于循環時會有不必要的開銷。
使用 Python generator
這也可以節省內存和提高性能。例如一個視頻流,你可以一個一個字節塊的發送,而不是整個流。例如:
>>> chunk = ( 1000 * i for i in xrange(1000))
>>> chunk
>>> chunk.next()
0
>>> chunk.next()
1000
>>> chunk.next()
2000
了解itertools模塊
該模塊對迭代和組合是非常有效的。讓我們生成一個列表[1,2,3]的所有排列組合,僅需三行Python代碼:
>>> import itertools
>>> iter = itertools.permutations([1,2,3])
>>> list(iter)
[(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]
學習bisect模塊保持列表排序
這是一個免費的二分查找實現和快速插入有序序列的工具。也就是說,你可以使用:
>>> import bisect
>>> bisect.insort(list, element)
你已將一個元素插入列表中, 而你不需要再次調用 sort() 來保持容器的排序, 因為這在長序列中這會非常昂貴.
理解Python列表,實際上是一個數組
Python中的列表實現并不是以人們通常談論的計算機科學中的普通單鏈表實現的。Python中的列表是一個數組。也就是說,你可以以常量時間 O(1) 檢索列表的某個元素,而不需要從頭開始搜索。這有什么意義呢? Python開發人員使用列表對象insert()時, 需三思.
例如:>>> list.insert(0,item)
在列表的前面插入一個元素效率不高, 因為列表中的所有后續下標不得不改變. 然而,您可以使用list.append()在列表的尾端有效添加元素. 優先選擇deque,如果你想快速的在插入或刪除時。它是快速的,因為在Python中的deque用雙鏈表實現。
使用Schwartzian Transform 的 sort()
原生的list.sort()函數是非常快的。 Python會按自然順序排序列表。有時,你需要非自然順序的排序。例如,你要根據服務器位置排序的IP地址。 Python支持自定義的比較,你可以使用list.sort(CMP()),這會比list.sort()慢,因為增加了函數調用的開銷。如果性能有問 題,你可以申請Guttman-Rosler Transform,基于Schwartzian Transform. 它只對實際的要用的算法有興趣,它的簡要工作原理是,你可以變換列表,并調用Python內置list.sort() - > 更快,而無需使用list.sort(CMP() )->慢。
Python裝飾器緩存結果
“@”符號是Python的裝飾語法。它不只用于追查,鎖或日志。你可以裝飾一個Python函數,記住調用結果供后續使用。這種技術被稱為memoization的。下面是一個例子:
>>> from functools import wraps
>>> def memo(f):
>>> cache = { }
>>> @wraps(f)
>>> def wrap(*arg):
>>> if arg not in cache: cache['arg'] = f(*arg)
>>> return cache['arg']
>>> return wrap
我們也可以對 Fibonacci 函數使用裝飾器:
>>> @memo
>>> def fib(i):
>>> if i < 2: return 1
>>> return fib(i-1) + fib(i-2)
這里的關鍵思想是:增強函數(裝飾)函數,記住每個已經計算的Fibonacci值;如果它們在緩存中,就不需要再計算了.
理解Python的GIL(全局解釋器鎖)
GIL是必要的,因為CPython的內存管理是非線程安全的。你不能簡單地創建多個線程,并希望Python能在多核心的機器上運行得更快。這是 因為 GIL將會防止多個原生線程同時執行Python字節碼。換句話說,GIL將序列化您的所有線程。然而,您可以使用線程管理多個派生進程加速程序,這些程 序獨立的運行于你的Python代碼外。
使用multiprocessing模塊實現真正的并發
因為GIL會序列化線程, Python中的多線程不能在多核機器和集群中加速. 因此Python提供了multiprocessing模塊, 可以派生額外的進程代替線程, 跳出GIL的限制. 此外, 你也可以在外部C代碼中結合該建議, 使得程序更快.
注意, 進程的開銷通常比線程昂貴, 因為線程自動共享內存地址空間和文件描述符. 意味著, 創建進程比創建線程會花費更多, 也可能花費更多內存. 這點在你計算使用多處理器時要牢記.
本地代碼
好了, 現在你決定為了性能使用本地代碼. 在標準的ctypes模塊中, 你可以直接加載已編程的二進制庫(.dll 或 .so文件)到Python中, 無需擔心編寫C/C++代碼或構建依賴. 例如, 我們可以寫個程序加載libc來生成隨機數。
然而, 綁定ctypes的開銷是非輕量級的. 你可以認為ctypes是一個粘合操作系庫函數或者硬件設備驅動的膠水. 有幾個如 SWIG, Cython和Boost 此類Python直接植入的庫的調用比ctypes開銷要低. Python支持面向對象特性, 如類和繼承. 正如我們看到的例子, 我們可以保留常規的C++代碼, 稍后導入. 這里的主要工作是編寫一個包裝器 (行 10~18).
像熟悉文檔一樣的熟悉Python源代碼
Python有些模塊為了性能使用C實現。當性能至關重要而官方文檔不足時,可以自由探索源代碼。你可以找到底層的數據結構和算法。
其他優化技巧
1. 如果需要交換兩個變量的值使用 a,b=b,a 而不是借助中間變量 t=a;a=b;b=t;
>>> from timeit import Timer
>>> Timer("t=a;a=b;b=t","a=1;b=2").timeit()
0.25154118749729365
>>> Timer("a,b=b,a","a=1;b=2").timeit()
0.17156677734181258
>>>
2. 在循環的時候使用 xrange 而不是 range;使用 xrange 可以節省大量的系統內存,因為 xrange() 在序列中每次調用只產生一個整數元素。而 range() 將直接返回完整的元素列表,用于循環時會有不必要的開銷。在 python3 中 xrange 不再存在,里面 range 提供一個可以遍歷任意長度的范圍的 iterator。
3. 使用局部變量,避免"global" 關鍵字。python 訪問局部變量會比全局變量要快得多,因此可以利用這一特性提升性能。
4. if done is not None 比語句 if done != None 更快,讀者可以自行驗證;
5. 在耗時較多的循環中,可以把函數的調用改為內聯的方式;
6. 使用級聯比較 "x < y < z" 而不是 "x < y and y < z";
7. while 1 要比 while True 更快(當然后者的可讀性更好);
8. build in 函數通常較快,add(a,b) 要優于 a+b。
結論
這些不能替代大腦思考. 打開引擎蓋充分了解是開發者的職責,使得他們不會快速拼湊出一個垃圾設計. 以上的Python建議可以幫助你獲得好的性能. 如果速度還不夠快, Python將需要借助外力:分析和運行外部代碼。
工具優化
有益的提醒,靜態編譯的代碼仍然重要. 僅例舉幾例, Chrome,Firefox,MySQL,MS Office 和 Photoshop都是高度優化的軟件,我們每天都在使用. Python作為解析語言,很明顯不適合. 不能單靠Python來滿足那些性能是首要指示的領域. 這就是為什么Python支持讓你接觸底層裸機基礎設施的原因, 將更繁重的工作代理給更快的語言如C. 這高性能計算和嵌入式編程中是關鍵的功能.
首先 拒絕調優誘惑
調優給你的代碼增加復雜性. 集成其它語言之前, 請檢查下面的列表. 如果你的算法是"足夠好", 優化就沒那么迫切了.
1. 你做了性能測試報告嗎?
2. 你能減少硬盤的 I/O 訪問嗎?
3. 你能減少網絡 I/O 訪問嗎?
4. 你能升級硬件嗎?
5. 你是為其它開發者編譯庫嗎?
6. 你的第三方庫軟件是最新版嗎?
對代碼優化的前提是需要了解性能瓶頸在什么地方,程序運行的主要時間是消耗在哪里,對于比較復雜的代碼可以借助一些工具來定位,python內置了 豐富的性能分析工具,如 profile,cProfile 與 hotshot 等。其中 Profiler是python 自帶的一組程序,能夠描述程序運行時候的性能,并提供各種統計幫助用戶定位程序的性能瓶頸。Python 標準模塊提供三種 profilers:cProfile,profile 以及 hotshot。
使用工具監控代碼 而不是直覺
速度的問題可能很微妙, 所以不要依賴于直覺. 感謝 "cprofiles" 模塊, 通過簡單的運行你就可以監控Python代碼.
1. “python -m cProfile myprogram.py”
2. 使用import profile模塊
import profile
def profileTest():
Total =1;
for i in range(10):
Total=Total*(i+1)
print Total
return Total
if __name__ == "__main__":
profile.run("profileTest()")
程序的運行結果如下:
其中輸出每列的具體解釋如下:
ncalls:表示函數調用的次數;
tottime:表示指定函數的總的運行時間,除掉函數中調用子函數的運行時間;
percall:(第一個 percall)等于 tottime/ncalls;
cumtime:表示該函數及其所有子函數的調用運行的時間,即函數開始調用到返回的時間;
percall:(第二個 percall)即函數運行一次的平均時間,等于 cumtime/ncalls;
filename:lineno(function):每個函數調用的具體信息;
如果需要將輸出以日志的形式保存,只需要在調用的時候加入另外一個參數。如 profile.run("profileTest()","testprof")。
對于大型應用程序,如果能夠將性能分析的結果以圖形的方式呈現,將會非常實用和直觀,常見的可視化工具有 Gprof2Dot,visualpytune,KCacheGrind 等,讀者可以自行查閱相關官網。
審查時間復雜度
控制以后, 提供一個基本的算法性能分析. 恒定時間是理想值. 對數時間復度是穩定的. 階乘復雜度很難擴展.
O(1) -> O(lg n) -> O(n lg n) -> O(n^2) -> O(n^3) -> O(n^k) -> O(k^n) -> O(n!)
使用第三方包
Python 性能優化除了改進算法,選用合適的數據結構之外,還有幾種關鍵的技術,比如將關鍵 python 代碼部分重寫成 C 擴展模塊,或者選用在性能上更為優化的解釋器等,這些在本文中統稱為優化工具。python 有很多自帶的優化工具,如 Psyco,Pypy,Cython,Pyrex 等,這些優化工具各有千秋。
Psycopsyco 是一個 just-in-time 的編譯器,它能夠在不改變源代碼的情況下提高一定的性能,Psyco 將操作編譯成有點優化的機器碼,其操作分成三個不同的級別,有"運行時"、"編譯時"和"虛擬時"變量。并根據需要提高和降低變量的級別。運行時變量只是 常規 Python 解釋器處理的原始字節碼和對象結構。一旦 Psyco 將操作編譯成機器碼,那么編譯時變量就會在機器寄存器和可直接訪問的內存位置中表示。同時 python 能高速緩存已編譯的機器碼以備今后重用,這樣能節省一點時間。但 Psyco 也有其缺點,其本身運行所占內存較大。目前 psyco 已經不在 python2.7 中支持,而且不再提供維護和更新了,對其感興趣的可以參考:
http://developer.51cto.com/art/201301/376509.htm
PypyPyPy 表示 "用 Python 實現的 Python",但實際上它是使用一個稱為RPython的 Python 子集實現的,能夠將 Python 代碼轉成 C, .NET, Java 等語言和平臺的代碼。PyPy 集成了一種即時 (JIT) 編譯器。和許多編譯器,解釋器不同,它不關心 Python代碼的詞法分析和語法樹。 因為它是用 Python 語言寫的,所以它直接利用 Python 語言的 Code Object。Code Object 是 Python 字節碼的表示,也就是說, PyPy直接分析 Python 代碼所對應的字節碼,這些字節碼即不是以字符形式也不是以某種二進制格式保存在文件中, 而在 Python 運行環境中。目前版本是 1.8. 支持不同的平臺安裝,windows 上安裝 Pypy 需要先下載 https://bitbucket.org/pypy/pypy/downloads/pypy-1.8-win32.zip, 然后解壓到相關的目錄,并將解壓后的路徑添加到環境變量 path 中即可。在命令行運行 pypy,如果出現如下錯誤:"沒有找到 MSVCR100.dll, 因此這個應用程序未能啟動,重新安裝應用程序可能會修復此問題",則還需要在微軟的官網上下載 VS 2010 runtime libraries 解決該問題。具體地址為 http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=5555
CythonCython 是用 python 實現的一種語言,可以用來寫 python 擴展,用它寫出來的庫都可以通過 import 來載入,性能上比 python 的快。cython 里可以載入 python 擴展 ( 比如 import math),也可以載入 c 的庫的頭文件 ( 比如 :cdef extern from "math.h"),另外也可以用它來寫 python 代碼。將關鍵部分重寫成 C 擴展模塊。
以上第三方工具可參考:
http://www.linuxidc.com/Linux/2012-07/66757p3.htm
總結
希望這些Python建議能讓你成為一個更好的開發者. 最后, 我需要指出, 追求性能極限是一個有趣的游戲, 而過度優化就會變成嘲弄了. 雖然Python授予你與C接口無縫集成的能力, 你必須問自己你花數小時的艱辛優化工作用戶是否買帳. 另一方面, 犧牲代碼的可維護性換取幾毫秒的提升是否值得. 團隊中的成員常常會感謝你編寫了簡潔的代碼. 盡量貼近Python的方式, 因為人生苦短. :)