一次跑偏之旅!
对于一个惯用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()再手工迭代差远了。盒盒~
- php 请求url获取状态码
function get_http_code($url) { $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $url); //设置URL c ...
- 01_StudentManager
package com.dao; import java.util.ArrayList; import java.util.Iterator; import java.util.List; impor ...
- JQuery强大的操控标签能力
jquery选择器 $("#myId") 选择id为myId的元素 $(".myClass") 选择class为myClass的元素 ...
- (转)MapReduce Design Patterns(chapter 6 (part 1))(十一)
Chapter 6. Metapatterns 这种模式不是解决某个问题的,而是处理模式的关系的.可以理解为“模式的模式”.首先讨论的是job链,把几个模式联合起来解决复杂的,有多个阶段要处理的问题. ...
- memcached asp.net
下载文件 memcached 1.解压缩文件到e:\memcached 2.命令行输入 e:\memcached\memcached.exe -d install' 3.命令行输入 e:\memcac ...
- python3 使用matplotlib画图问题
保存图片的问题 在画子图并保存图片的时候,用如下代码保存的图片总是不能显示,但是在运行的过程中能够正常显示图片. # coding:utf-8 from pylab import * # 创建子图 f ...
- Sublime 笔记
1. 安装PackageControl 官网:https://packagecontrol.io/ v2: import urllib2,os,hashlib; h = '2915d1851351e5 ...
- JAXP使用Stax API时格式化输出XML
最近项目中需要生成XBRL instance,对于XML读写和验证进行了一些学习.由于Stax API不支持格式化输出,默认全都写在一行上,网上也没有搜到现成的东西,自己写了一个格式化输出的帮助类. ...
- 使用PE工具箱 ughost 恢复系统导致被捆绑一堆软件的问题
很多的pe系统,都有这么一个东西, 没啥技术含量,就是给ghost 加一个UI 而已 , 但是使用这玩意恢复系统,会被感染一些捆绑软件 ,开机之后就会卡顿, 并下载一堆软件 包括 爱奇艺 , 百度搜 ...
- web.xml配置详解(2)
1 定义头和根元素 部署描述符文件就像所有XML文件一样,必须以一个XML头开始.这个头声明可以使用的XML版本并给出文件的字符编码.DOCYTPE声明必须立即出现在此头之后.这个声明告诉服务器适用的 ...