一次跑偏之旅!
對於一個慣用C++的人來講,使用Python這種語言的一大障礙就是許多集合類型的操做效率並不如傳統的經典數據結構那樣直觀可見,以及許多實際上涉及到內存分配、對象複製之類的耗時操做被隱藏在看似簡單的接口之中。加上Python的文檔只強調如何使用,大部分時候都對實現的細節和效率語焉不詳。這使我在使用Python時,會有一種比用C++更加當心翼翼的心態。當有許多個方式來加工一個數據集時,我不得不仔細考慮哪種方式纔是效率最高的,由於沒法從文檔中得到相關的信息,因此只能靠經驗推測或是閱讀源碼來判斷,這常常比用C++更加費時和困難。
雖然Python的優點在於其開發效率、統一的類庫和簡潔的語法,但對於全部從事企業級開發的人來講,顯然任何語言的效率都是值得重視的。
因此,在僞裝不關心效率地寫了幾天以後,我今天花了些時間來嘗試判斷一個小問題:
當須要渲染一個dict的全部value時,究竟應該向RenderContext裏塞一個怎樣的數據集對象纔是最高效的?
從最直觀的寫法開始:
data = {'a' : '1', 'b' : '2'}
ctx = Context({'data' : data.values()})
這裏使用到的dict.values()函數會新建一個列表,把dict的全部value複製到其中 —— 顯然效率不夠高。一個改進的寫法是使用迭代器:
data = {'a' : '1', 'b' : '2'}
ctx = Context({'data' : data.itervalues()})
這是一個效率很高的實現,可是很遺憾的是,它存在一個問題。正是這個問題致使我下決心來探尋其中的實現細節:當使用iterator來構造Context時,只有第一個使用該數據集的for Tag可以正確的渲染出數據,若是在一個模板中存在多個地方須要渲染同一個數據集,後續的for Tag所有隻能輸出空列表。
產生這個問題的緣由是迭代器指針在第一次遍歷完以後,指針位置到達了列表末尾,在下一次遍歷時,迭代器並無重置,因此天然沒法取到數據。
坦白說,我認爲這是一個語義範疇的Bug,渲染引擎應該考慮這種狀況並確保屢次渲染所取到的數據是一致的。因此接下來我想看看有沒有什麼辦法來解決這個Bug,說不定還能成爲我對開源項目的第一個commit...
首先,最簡單的辦法是在使用完迭代器以後reset一下,然而iterator並無reset接口。
或者,在每次使用都使用原始迭代器的拷貝,然而iterator一樣沒有clone接口。找到個itertools.tee函數,號稱能夠複製迭代器,可是其實只能接收iterable參數,傳遞iterator參數給它一樣會致使上面的問題。
d= {'a':'1','d':'2'}
import itertools
di = d.itervalues()
di1 = itertools.tee(di,1)
list(di1[0])
>>> ['1', '2']
list(di)
>>> []
不得不吐槽一句,各類翻譯真貽害不淺,搞得我還覺得這個tee是元數據語言的黑科技,鬧半天發現只不過一個從iterable批量產生iterator的util。
既不能reset,又不能clone的話,那麼就只剩兩個選擇了,一個是從Django渲染引擎的實現中看有沒有辦法,二是不用iterator來構造Context。
對於後者,一個簡單的辦法是本身實現一個iterable,用來代理dict.itervalues,而後用這個iterable對象來構造Context,這樣不須要拷貝數據集,還能夠保證每次使用的迭代器都是新的:
class iterable4dictval():
def __init__(self, dict_obj):
self.dict_obj = dict_obj
def __iter__(self):
if self.dict_obj is None or not isinstance(self.dict_obj, dict):
return None
return self.dict_obj.itervalues()
data = {'a' : '1', 'b' : '2'}
ctx = Context({'data' : iterable4dictval(data)})
這是一個能工做的實現,調用方的開銷也很小,可是效率是否真的高呢?嘿!Django的實現告訴你然並卵...
那麼,來看看Django渲染引擎的實現,對於安裝好的Django,for Tag的實現代碼在Lib\site-packages\django\template\defaulttags.py文件中,它對數據集的處理過程大概是這樣的:
class ForNode(Node):
def render(self, context):
...
try:
values = self.sequence.resolve(context, True)
except VariableDoesNotExist:
values = []
if values is None:
values = []
if not hasattr(values, '__len__'):
values = list(values)
len_values = len(values)
if len_values < 1:
context.pop()
return self.nodelist_empty.render(context)
for i, item in enumerate(values):
...
這個實現會先判斷Data Object是否有__len__屬性,沒有的話就會先轉換成一個list。什麼樣的對象支持或應該支持__len__屬性呢?Python小白的我還特地先百度了一番:
簡單來講呢,__len__基本上和len()的支持是對應的,而文檔裏說len函數支持全部的sequence和collection類型,也就是string, bytes, tuple, list, range, dictionary, set這些。
顯然,iterable和iterator是不支持len的,也就是說,若是使用iterable或iterator來構造Context,那麼Django在渲染前,仍是會把全部數據都轉存到一個新建的list裏去...得!調用方省下的效率,全都在實現中還回去了!
Django的開發者顯然不至於腦殘到不知道iterator,那麼爲何要這樣實現?ForNode.render實現的其它部分揭示了答案,代碼就不列了。咱們看看for Tag支持的一些變量:
forloop.counter
forloop.counter0
forloop.revcounter
forloop.revcounter0
forloop.first
forloop.last
forloop.parentloop
其它的都好說,惟獨revcounter,若是不知道數據集的長度,要支持這個變量就難了。對於iterable,或許能夠作兩次遍歷,一次計算長度,一次渲染;但對iterator,除了轉換爲列表,還真沒有什麼好的辦法,更況且還可能有形狀提到的屢次渲染需求問題。因此,Django乾脆直接把把iterable和iterator都轉成list。
最後,回到正題,若是要渲染dict的全部value,到底怎樣構造Context纔是最高效的?若是沒看過Django的實現,或許咱們會認爲使用迭代器是最高效的方法之一,可是看過以後,最高效的辦法只有一個,沒有之一。那就是直接用dict來構造Context,而後這樣寫模板:
{% for key, value in data.items %}
{{ key }}: {{ value }}
{% endfor %}
注意data變量就是字典,而不是data.items。
後話:
Andorid的文件枚舉接口,也存在一個相似的效率問題,當初也是搞得我很無語。java.io.File.dir()有一個重載是帶一個過濾器參數,返回一個通過過濾的文件列表,看起來這比返回全部子文件列表的開銷要小一點。然而,你們看看這個實現:
public String[] list(FilenameFilter filter) {
String[] filenames = list();
if (filter == null || filenames == null) {
return filenames;
}
List<String> result = new ArrayList<String>(filenames.length);
for (String filename : filenames) {
if (filter.accept(this, filename)) {
result.add(filename);
}
}
return result.toArray(new String[result.size()]);
}
先獲取一個全部子文件的列表,再用for循環處理一遍,把符合條件的項再放到一新列表中去。也就是說,這貨實際上是建立兩個列表的開銷,效率比調用方直接用list()再手工迭代差遠了。盒盒~