一次跑偏之旅!
 
对于一个惯用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()再手工迭代差远了。盒盒~
 

Django模板输出Dict所有Value的效率问题的更多相关文章

  1. 怎么通过django模板输出双花括号{{}}

    https://segmentfault.com/q/1010000000685399

  2. Django模板层

    一:模板简介 二:模板语法值变量 三: 模板之过滤器 四: 模板之标签 五:自定义标签和过滤器   一:模板简介 def current_datetime(request): now=datetime ...

  3. Django——模板层(template)(模板语法、自定义模板过滤器及标签、模板继承)

    前言:当我们想在页面上给客户端返回一个当前时间,一些初学者可能会很自然的想到用占位符,字符串拼接来达到我们想要的效果,但是这样做会有一个问题,HTML被直接硬编码在 Python代码之中. 1 2 3 ...

  4. django模板(一)

    模板(一) 实验简介 在前一章中,你可能已经注意到我们在例子视图中返回文本的方式有点特别. 也就是说,HTML被直接硬编码在 Python 代码之中. def current_datetime(req ...

  5. Django模板修炼

    引言:由于我们在使用Django框架时,不会将HTML代码采用硬编码的方式,因为会有以下缺点: 1:对页面设计进行的任何改变都必须对 Python 代码进行相应的修改. 站点设计的修改往往比底层 Py ...

  6. Django—模板

    索引 一.模板语言 1.1 变量 1.2 标签 1.3 过滤器 1.4 自定义过滤器 1.5 注释 二.模板继承 三.HTML转义 四.CSRF 五.验证码 六.反向解析 模板 作为Web框架,Dja ...

  7. django——模板层

    每一个Web框架都需要一种很便利的方法用于动态生成HTML页面. 最常见的做法是使用模板. 模板包含所需HTML页面的静态部分,以及一些特殊的模版语法,用于将动态内容插入静态部分. 说白了,模板层就是 ...

  8. 测试开发之Django——No7.Django模板中的过滤器

    1.add 将参数添加到值. 例如: {{ value|add:"2" }} 如果value是4,那么输出将是6. 此过滤器将首先尝试将两个值强制转换为整数.如果失败,它将尝试将值 ...

  9. django模板(template)

    模板层(template) 你可能已经注意到我们在例子视图中返回文本的方式有点特别. 也就是说,HTML被直接硬编码在 Python代码之中. 1 2 3 4 def current_datetime ...

随机推荐

  1. HDU 3473 Minimum Sum 划分树,数据结构 难度:1

    http://acm.hdu.edu.cn/showproblem.php?pid=3473 划分树模板题目,需要注意的是划分树的k是由1开始的 划分树: 参考:http://blog.csdn.ne ...

  2. jQuery动画与特效

    参考:jQuery权威指南jQuery初步jQuery选择器jQuery操作domjQuery操作dom事件jQuery插件jQuery操作AjaxjQuery动画与特效jQuery实现导航栏jQue ...

  3. 剑指offer--47.数据流中的中位数

    时间限制:1秒 空间限制:32768K 热度指数:122511 算法知识视频讲解 题目描述 如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值.如 ...

  4. 编写configure.ac

    configure.ac由一些宏组成(如果已经有源代码,你可以运行autoscan来产生一个configure.scan文件,在此基础修改成configure.ac将更加方便) 最基本的组成可以是下面 ...

  5. IE11降级到IE8

  6. swift metal ios8 关键字.

    swift metal ios8  关键字. 4000API. 无所谓谁打败谁吧. 行业内用户用的多 资源多 问题容易解决. 今年明显unity 火热程度非常. 然,万变不离其中. 对于游戏产品来说, ...

  7. IOS开发 警告 All interface orientations must be supported unless the app requires full screen.

    在IOS开发中遇到警告  All interface orientations must be supported unless the app requires full screen. 只要勾上R ...

  8. Postfix常用命令和邮件队列管理(queue)

    本文主要介绍一下postfix的常用命令及邮件队列的管理: Postfix有以下四种邮件队列,均由管理队列的进程统一进行管理: maildrop:本地邮件放置在maildrop中,同时也被拷贝到inc ...

  9. 【剑指offer】找出数组中任意重复的数字(不修改数组),C++实现

    原创博文,转载请注明出处! # 题目 在一个长度为n+1的数组里的所有数字都在1~n的范围内,所以数组中至少有一个数字是重复的.请找出数组中任意一个重复的数字,但不能修改输入的数组.例如,如果输入长度 ...

  10. 【pandas】pandas.Series.str.split()---字符串分割

    原创博文,转载请注明出处! 本文代码的github地址       series中的元素均为字符串时,通过str.split可将字符串按指定的分隔符拆分成若干列的形式. 例子: 拆分以逗号为分隔符的字 ...