程序運(yùn)行太慢,想要提速,但不使用復(fù)雜的技術(shù)如 C 擴(kuò)展或 JIT 編譯器。
解決方案
程序優(yōu)化的第一準(zhǔn)則是“不要優(yōu)化”,第二準(zhǔn)則是“不要優(yōu)化那些不重要的部分”?;谶@兩個原則,如果你的程序運(yùn)行得很慢,你得先找出影響性能的問題所在。
多數(shù)時候我們發(fā)現(xiàn)程序把大量的時間花在幾個熱點(diǎn)位置,比如處理數(shù)據(jù)的內(nèi)層循環(huán)。一旦確認(rèn)了這些熱點(diǎn),就可以使用以下各小節(jié)中介紹的技術(shù)讓程序運(yùn)行得更快。
使用函數(shù)
很多人開始使用 Python 時都是用它來編寫一些簡單的腳本。最開始時,很容易陷入只管編寫代碼而不重視程序結(jié)構(gòu)的怪圈。例如:
# somescript.py
import sys
import csv
with open(sys.argv[1]) as f:
for row in csv.reader(f):
# Some kind of processing
pass
一個鮮為人知的事實(shí)是,像上面這樣定義在全局范圍內(nèi)的代碼比定義在函數(shù)中的代碼要慢。速度的差異與局部變量與全局變量的實(shí)現(xiàn)機(jī)制有關(guān)(涉及局部變量的操作要更快)。因此,如果想讓程序運(yùn)行得更快,可以將腳本中的語句放入函數(shù)中即可:
# somescript.py
import sys
import csv
def main(filename):
with open(filename) as f:
for row in csv.reader(f):
# Some kind of processing
pass
main(sys.argv[1])
運(yùn)行速度的差異與具體執(zhí)行的任務(wù)有關(guān),但根據(jù)經(jīng)驗(yàn),提升 15% ~ 30% 的情況很常見。
消除屬性訪問
每次使用句點(diǎn)操作符(.)來訪問對象的屬性都會帶來開銷。在底層,這會觸發(fā)調(diào)用特殊的方法。
通??梢杂?from module import name 的導(dǎo)入形式以及選擇性地使用綁定方法(bound method)來避免出現(xiàn)屬性查詢操作。我們用下面的代碼片段來加以說明:
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)
當(dāng)在我們的機(jī)器上測試時,這個程序運(yùn)行了大約 40 秒?,F(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
修改后的版本運(yùn)行時間大約是 29 秒。唯一不同之處就是消除了屬性訪問。用 sqrt() 代替了 math.sqrt()。result.append() 方法被賦給一個局部變量 result_append,然后在內(nèi)部循環(huán)中使用它。
但是,必須強(qiáng)調(diào)的是,只有在頻繁執(zhí)行的代碼中做這些修改才有意義,比如在循環(huán)中。因此,這種優(yōu)化技術(shù)適用的場景需要經(jīng)過精心挑選。
理解變量所處的位置
前述提及,訪問局部變量比全局變量要快。對于需要頻繁訪問的名稱,想提高運(yùn)行速度,可以通過盡量讓這些變量盡可能成為局部變量來實(shí)現(xiàn)。例如:
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 方法已經(jīng)從 math 模塊中提取出來并放置在一個局部變量中。如果運(yùn)行這份代碼,執(zhí)行時間大約是 25 秒,這比上一個版本的 29 秒又有所提升。根本原因就是查找局部變量比全局變量要快。
當(dāng)使用類時,局部參數(shù)同樣能起到提速的效果。一般來說,查找像 self.name 這樣的值會比訪問一個局部變量要慢很多。在內(nèi)層循環(huán)中將需要經(jīng)常訪問的屬性移到局部變量中來會很劃算。例如:
# 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)
避免不必要的抽象
裝飾器(decorator)、屬性(property)或者描述符(descriptor)包裝過的代碼,運(yùn)行速度通常會變慢。參考以下代碼:
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
>>>
使用內(nèi)建的容器
內(nèi)建的數(shù)據(jù)類型比如字符串、元組、列表、集合以及字典都是用 C 語言實(shí)現(xiàn)的,速度非???。如果需要構(gòu)建自己的數(shù)據(jù)結(jié)構(gòu)作為替代(例如鏈表、二叉樹等),想在性能上達(dá)到內(nèi)建的速度幾乎不可能,因此還是盡量使用內(nèi)建的數(shù)據(jù)結(jié)構(gòu)吧。
避免產(chǎn)生不必要的數(shù)據(jù)結(jié)構(gòu)或者拷貝動作
有時候程序員可能會創(chuàng)建一些不必要的數(shù)據(jù)結(jié)構(gòu),比如下面的代碼:
values = [x for x in sequence]
squares = [x*x for x in values]
也許這里的想法是首先將一些值收集到一個列表中,然后使用列表推導(dǎo)來執(zhí)行操作。不過,第一個列表完全沒有必要,可以簡單的像下面這樣寫:
squares?=?[x*x?for?x?in?sequence]
與此相關(guān),還要注意下那些對Python的共享數(shù)據(jù)機(jī)制過于偏執(zhí)的程序所寫的代碼。有些人并沒有很好的理解或信任Python的內(nèi)存模型,濫用 copy.deepcopy()?之類的函數(shù)。通常在這些代碼中是可以去掉復(fù)制操作的。
討論
在進(jìn)行優(yōu)化之前,有必要研究一下使用的算法。選擇一個復(fù)雜度為 O(n log n) 的算法要比你去調(diào)整一個復(fù)雜度為 O(n**2) 的算法所帶來的性能提升要大得多。
如果優(yōu)化代碼勢在必行,那么請從整體考慮。作為一般準(zhǔn)則,不要對程序的每一個部分都去優(yōu)化,因?yàn)檫@些修改會導(dǎo)致代碼難以閱讀和理解。你應(yīng)該專注于優(yōu)化產(chǎn)生性能瓶頸的地方,比如內(nèi)部循環(huán)。
還要注意一些小的優(yōu)化的結(jié)果。比如下面創(chuàng)建字典的兩種方式:
a = {
'name' : 'AAPL',
'shares' : 100,
'price' : 534.22
}
b = dict(name='AAPL', shares=100, price=534.22)
后面一種寫法更簡潔一些(你不需要在關(guān)鍵字上輸入引號)。不過,如果你將這兩個代碼片段進(jìn)行性能測試對比時,會發(fā)現(xiàn)使用 dict()?的方式會慢了3倍??吹竭@個,你是不是有沖動把所有使用 dict()?的代碼都替換成第一種。不過,聰明的程序員只會關(guān)注他應(yīng)該關(guān)注的地方,比如內(nèi)部循環(huán)。在其他地方,這點(diǎn)性能損失沒有什么影響。
如果你的優(yōu)化要求比較高,本節(jié)的這些簡單技術(shù)滿足不了,那么你可以研究下基于即時編譯(JIT)技術(shù)的一些工具。例如,PyPy 工程是 Python 解釋器的另外一種實(shí)現(xiàn),它會分析你的程序運(yùn)行并對那些頻繁執(zhí)行的部分生成本機(jī)機(jī)器碼。它有時候能極大的提升性能,通??梢越咏?C 代碼的速度。不過可惜的是,到寫這本書為止,PyPy 還不能完全支持 Python3。因此,這個是你將來需要去研究的。你還可以考慮下 Numba 工程, Numba 是一個在你使用裝飾器來選擇 Python 函數(shù)進(jìn)行優(yōu)化時的動態(tài)編譯器。這些函數(shù)會使用LLVM被編譯成本地機(jī)器碼。它同樣可以極大的提升性能。但是,跟 PyPy 一樣,它對于 Python 3 的支持現(xiàn)在還停留在實(shí)驗(yàn)階段。
最后我引用John Ousterhout說過的話作為結(jié)尾:“最好的性能提升就是從不工作轉(zhuǎn)變?yōu)榭梢怨ぷ?/strong>”。直到你真的需要優(yōu)化的時候再去考慮它。確保你程序正確的運(yùn)行通常比讓它運(yùn)行更快要更重要一些(至少開始是這樣的)。
參考
《Python Cookbook》第三版
http://python3-cookbook.readthedocs.org/zh_CN/latest/文章來源:http://www.zghlxwxcb.cn/news/detail-830378.html
關(guān)于簡說基因
-
生信平臺
Galaxy中國(UseGalaxy.cn)致力于打造中國人的云上生物信息基礎(chǔ)設(shè)施。大量在線工具免費(fèi)使用。無需安裝,用完即走。活躍的用戶社區(qū),隨時交流使用心得。文章來源地址http://www.zghlxwxcb.cn/news/detail-830378.html
-
生信培訓(xùn)
簡說基因的生信培訓(xùn)班,榮獲學(xué)員的一致好評。如果你也對生物信息學(xué)感興趣,歡迎來跟簡說基因,學(xué)真生信。
-
生信分析
我們能夠承接所有 NGS 組學(xué)數(shù)據(jù)分析業(yè)務(wù),包括但不限于 WGS / WES / RNA-seq 等?;蚪M組裝、注釋,以及各種重測序業(yè)務(wù)都可以與簡說基因合作。
到了這里,關(guān)于Python在生物信息學(xué)中的應(yīng)用:讓你的程序運(yùn)行得更快的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!