一次跑偏之旅!
对于一个惯用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()再手工迭代差远了。盒盒~
- 027——VUE中事件修饰符:stop prevent self capture
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- MySQL使用通用二进制格式安装
CentOS7安装MySQL的方法之通用二进制格式
- 二十三、DBMS_METADATA(提供提取数据库对象的完整定义的接口)
1.概述 作用:提供提取数据库对象的完整定义的接口.这些定义可以用XML或SQL DDL格式描述.提供两种类型接口:可编程控制的接口:用于Ad Hoc查询的简单接口. 2.包的组成 dbms_meta ...
- ElasticSearch6.0 高级应用之 多字段聚合Aggregation(二)
ElasticSearch6.0 多字段聚合网上完整的资料很少 ,所以作者经过查阅资料,编写了聚合高级使用例子 例子是根据电商搜索实际场景模拟出来的 希望给大家带来帮助! 下面我们开始吧! 1. 创建 ...
- mifi随身wifi选购
一款优秀的随身wifi不光要信号好,更要电量足 ,网速快.影响这个三个问题的主要因素就是cpu.so咱们从cpu的角度来分析下mifi 机器型号(cpu型号) TP 961 52000 (MDM962 ...
- 联想北研实习生面试-嵌入式Linux研发工程师
8月中旬暑假去联想北研参加了实习生面试,面试职位是嵌入式Linux研发工程师.投完简历第二天,主管回复我邮件,意思是说随时来面试,到北研时候给他打个电话就行.于是我回复条短信表示感谢,并约好时间第二天 ...
- .net 枚举(Enum)使用总结
在实际问题中,有些变量的取值被限定在一个有限的范围内.例如,一个星期内只有七天,一年只有十二个月,性别只有男跟女等等.如果把这些量说明为整型.字符型或其它类型显然是不妥当的.为此,C#提供了一种称为“ ...
- c# DataTable行转列
/// <summary> /// datatable行转列 /// </summary> /// <param name="dtSrc">来源 ...
- 微信小程序页面跳转方法汇总
微信小程序前端页面跳转有多种方式,汇总如下: Tips: 小程序前端的页面跳转之后,跳转之前的页面并不会凭空消失,而是存进了一个类似“页面栈”的空间里: 只有当这个所谓的“页面栈”满了之后页面才会退出 ...
- libcurl 错误码总结
下载出现这种错误(Requested range was not delivered by the server ),说明是重复下载,删掉本地的再下载就不会出现了