Django模板输出Dict全部Value的效率问题

一次跑偏之旅!
 
对于一个惯用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()再手工迭代差远了。盒盒~
相关文章
相关标签/搜索