優(yōu)化Python代碼使其加快作用域內(nèi)的查找
我將示范微優(yōu)化(micro optimization)如何提升python代碼5%的執(zhí)行速度。5%!同時(shí)也會(huì)觸怒任何維護(hù)你代碼的人。
但實(shí)際上,這篇文章只是解釋一下你偶爾會(huì)在標(biāo)準(zhǔn)庫或者其他人的代碼中碰到的代碼。我們先看一個(gè)標(biāo)準(zhǔn)庫的例子,collections.OrderedDict類:
def __setitem__(self, key, value, dict_setitem=dict.__setitem__): if key not in self: root = self.__root last = root[0] last[1] = root[0] = self.__map[key] = [last, root, key] return dict_setitem(self, key, value)
注意最后一個(gè)參數(shù):dict_setitem=dict.__setitem__。如果你仔細(xì)想就會(huì)感覺有道理。將值關(guān)聯(lián)到鍵上,你只需要給__setitem__傳遞三個(gè)參數(shù):要設(shè)置的鍵,與鍵關(guān)聯(lián)的值,傳遞給內(nèi)建dict類的__setitem__類方法。等會(huì),好吧,也許最后一個(gè)參數(shù)沒什么意義。
作用域查詢
為了理解到底發(fā)生了什么,我們看下作用域。從一個(gè)簡單問題開始:在一個(gè)python函數(shù)中,如果遇到了一個(gè)名為open的東西,python如何找出open的值?
# <GLOBAL: bunch of code here> def myfunc(): # <LOCAL: bunch of code here> with open('foo.txt', 'w') as f: pass
簡單作答:如果不知道GLOBAL和LOCAL的內(nèi)容,你不可能確定open的值。概念上,python查找名稱時(shí)會(huì)檢查3個(gè)命名空間(簡單起見忽略嵌套作用域):
局部命名空間
全局命名空間
內(nèi)建命名空間
所以在myfunc函數(shù)中,如果嘗試查找open的值時(shí),我們首先會(huì)檢查本地命名空間,然后是全局命名空間,接著內(nèi)建命名空間。如果在這3個(gè)命名空間中都找不到open的定義,就會(huì)引發(fā)NameError異常。
作用域查找的實(shí)現(xiàn)
上面的查找過程只是概念上的。這個(gè)查找過程的實(shí)現(xiàn)給予了我們探索實(shí)現(xiàn)的空間。
def foo(): a = 1 return a def bar(): return a def baz(a=1): return a
我們看下每個(gè)函數(shù)的字節(jié)碼:
>>> import dis >>> dis.dis(foo) 2 0 LOAD_CONST 1 (1) 3 STORE_FAST 0 (a) 3 6 LOAD_FAST 0 (a) 9 RETURN_VALUE >>> dis.dis(bar) 2 0 LOAD_GLOBAL 0 (a) 3 RETURN_VALUE >>> dis.dis(baz) 2 0 LOAD_FAST 0 (a) 3 RETURN_VALUE
注意foo和bar的區(qū)別。我們立即就可以看到,在字節(jié)碼層面,python已經(jīng)判斷了什么是局部變量、什么不是,因?yàn)閒oo使用LOAD_FAST,而bar使用LOAD_GLOBAL。
我們不會(huì)具體闡述python的編譯器如何知道何時(shí)生成何種字節(jié)碼(也許那是另一篇文章的范疇了),但足以理解,python在執(zhí)行函數(shù)時(shí)已經(jīng)知道進(jìn)行何種類型的查找。
另一個(gè)容易混淆的是,LOAD_GLOBAL既可以用于全局,也可以用于內(nèi)建命名空間的查找。忽略嵌套作用域的問題,你可以認(rèn)為這是“非局部的”。對應(yīng)的C代碼大概是[1]:
case LOAD_GLOBAL: v = PyObject_GetItem(f->f_globals, name); if (v == NULL) { v = PyObject_GetItem(f->f_builtins, name); if (v == NULL) { if (PyErr_ExceptionMatches(PyExc_KeyError)) format_exc_check_arg( PyExc_NameError, NAME_ERROR_MSG, name); goto error; } } PUSH(v);
即使你從來沒有看過CPython的C代碼,上面的代碼已經(jīng)相當(dāng)直白了。首先,檢查我們查找的鍵名是否在f->f_globals(全局字典)中,然后檢查名稱是否在f->f_builtins(內(nèi)建字典)中,最后,如果上面兩個(gè)位置都沒找到,就會(huì)拋出NameError異常。
將常量綁定到局部作用域
現(xiàn)在我們再看最開始的代碼例子,就會(huì)理解最后一個(gè)參數(shù)其實(shí)是將一個(gè)函數(shù)綁定到局部作用域中的一個(gè)函數(shù)上。具體是通過將dict.__setitem__賦值為參數(shù)的默認(rèn)值。這里還有另一個(gè)例子:
def not_list_or_dict(value): return not (isinstance(value, dict) or isinstance(value, list)) def not_list_or_dict(value, _isinstance=isinstance, _dict=dict, _list=list): return not (_isinstance(value, _dict) or _isinstance(value, _list))
這里我們做同樣的事情,把本來將會(huì)在內(nèi)建命名空間中的對象綁定到局部作用域中去。因此,python將會(huì)使用LOCAL_FAST而不是LOAD_GLOBAL(全局查找)。那么這到底有多快呢?我們做個(gè)簡單的測試:
$ python -m timeit -s 'def not_list_or_dict(value): return not (isinstance(value, dict) or isinstance(value, list))' 'not_list_or_dict(50)' 1000000 loops, best of 3: 0.48 usec per loop $ python -m timeit -s 'def not_list_or_dict(value, _isinstance=isinstance, _dict=dict, _list=list): return not (_isinstance(value, _dict) or _isinstance(value, _list))' 'not_list_or_dict(50)' 1000000 loops, best of 3: 0.423 usec per loop
換句話說,大概有11.9%的提升 [2]。比我在文章開始處承諾的5%還多!
還有更多內(nèi)涵
可以合理地認(rèn)為,速度提升在于LOAD_FAST讀取局部作用域,而LOAD_GLOBAL在檢查內(nèi)建作用域之前會(huì)先首先檢查全局作用域。上面那個(gè)示例函數(shù)中,isinstance、dict、list都位于內(nèi)建命名空間。
但是,還有更多。我們不僅可以使用LOAD_FAST跳過多余的查找,它也是一種不同類型的查找。
上面C代碼片段給出了LOAD_GLOBAL的代碼,下面是LOAD_FAST的:
case LOAD_FAST: PyObject *value = fastlocal[oparg]; if (value == NULL) { format_exc_check_arg(PyExc_UnboundLocalError, UNBOUNDLOCAL_ERROR_MSG, PyTuple_GetItem(co->co_varnames, oparg)); goto error; } Py_INCREF(value); PUSH(value); FAST_DISPATCH()
我們通過索引一個(gè)數(shù)組獲取局部值。雖然沒有直接出現(xiàn),但是oparg只是那個(gè)數(shù)組的一個(gè)索引。
現(xiàn)在聽起來才合理。我們第一個(gè)版本的not_list_or_dict要進(jìn)行4個(gè)查詢,每個(gè)名稱都位于內(nèi)建命名空間,它們只有在查找全局命名空間之后才會(huì)查詢。這就是8個(gè)字典鍵的查詢操作了。相比之下,not_list_or_dict的第二版中,直接索引C數(shù)組4次,底層全部使用LOAD_FAST。這就是為什么局部查詢更快的原因。
總結(jié)
現(xiàn)在當(dāng)下次你在其他人代碼中看到這種例子,就會(huì)明白了。
最后,除非確實(shí)需要,請不要在具體應(yīng)用中進(jìn)行這類優(yōu)化。而且大部分時(shí)間你都沒必要做。但是如果時(shí)候到了,你需要擠出最后一點(diǎn)性能,就需要搞懂這點(diǎn)。
腳注
[1]注意,為了更易讀,上面的代碼中我去掉了一些性能優(yōu)化。真正的代碼稍微有點(diǎn)復(fù)雜。
[2]示例函數(shù)事實(shí)上沒有做什么有價(jià)值的東西,也沒進(jìn)行IO操作,大部分是受python VM循環(huán)的限制。
相關(guān)文章
python?函數(shù)定位參數(shù)+關(guān)鍵字參數(shù)+inspect模塊
這篇文章主要介紹了python?函數(shù)定位參數(shù)+關(guān)鍵字參數(shù)+inspect模塊,文章圍繞主題展開詳細(xì)的相關(guān)資料,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-05-05Python破解BiliBili滑塊驗(yàn)證碼的思路詳解(完美避開人機(jī)識(shí)別)
這篇文章主要介紹了Python破解BiliBili滑塊驗(yàn)證碼的思路,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-02-02Python自動(dòng)化辦公之圖片轉(zhuǎn)PDF的實(shí)現(xiàn)
實(shí)現(xiàn)圖片轉(zhuǎn)換成PDF文檔的操作方法有很多,綜合對比以后感覺fpdf這個(gè)模塊用起來比較方便而且代碼量相當(dāng)少。所以本文將利用Python語言實(shí)現(xiàn)圖片轉(zhuǎn)PDF,感興趣的可以了解一下2022-04-04使用Python分析數(shù)據(jù)并進(jìn)行搜索引擎優(yōu)化的操作步驟
在互聯(lián)網(wǎng)時(shí)代,網(wǎng)站數(shù)據(jù)是一種寶貴的資源,可以用來分析用戶行為、市場趨勢、競爭對手策略等,本文將介紹如何使用Python爬取網(wǎng)站數(shù)據(jù),并進(jìn)行搜索引擎優(yōu)化,,需要的朋友可以參考下2023-08-08跟老齊學(xué)Python之大話題小函數(shù)(2)
上篇文章我們講訴了map 和lambda函數(shù)的使用,本文我們繼續(xù)來看看reduce和filter函數(shù),有需要的朋友可以參考下2014-10-10