首页 > 代码库 > 探寻Python中如何同时迭代多个iterable对象
探寻Python中如何同时迭代多个iterable对象
题外话:
最近因为课程需要开始深入了解Python语言。因为以前一直用的Java、C++等强类型的静态语言,现在突然使用Python确实感受到了很大的不同。
直观感觉就是,在Python中总是能找到一些让代码变得精巧、简洁、高效、美观的写法,使得初学者在写代码的过程充满了惊喜,从而渐渐喜欢上Python。而且Python的官方手册阅读起来感觉非常好,很多问题都描述的很清楚。不过总体来说,还是觉得Java大法好:)
Python中一个非常有用的语法就是for in循环跟iterable对象的结合,适当的使用它可以让代码变得更加赏心悦目。关于这一点一个被广泛引用的例子就是文本文件操作,这里也照搬一下。普通的文件操作:
with open("test.txt") as fp: while True: line=fp.readline() if line=="": break do_something_with(line)
引入for in循环以后的文件操作:
with open("test.txt") as fp: for line in fp: do_something_with(line)
明显的,代码一下子就简洁清晰了很多!由于Python里的file是iterable对象,所以可以直接被for in迭代。但是有个问题,这里的for in一次只能迭代一个序列。在这里也就是一次只能从一个文件里面读取一行,然后做一些处理。但是如果想每次从两个文件里面各读取一行,然后做一些处理,比如说文本对比,能做到吗?也就是说,Python中如何同时迭代多个序列,或者至少是同时迭代两个等长的序列?一个勉强可以接受的写法:
with open("a.txt") as fa, open("b.txt") as fb: try: while True: do_something_with(fa.next(), fb.next()) except StopIteration: pass
但是还可以更加简洁一些吗?可以假想,如果语法支持下面的写法就好了:
with open("a.txt") as fa, open("b.txt") as fb: for a,b in fa,fb: do_something_with(a, b)
但实际上这是不行的,这里会得到“ValueError: too many values to unpack ”,因为in后面的fa,fb被看做了一个序列,也就是下面的语句是合法的:
for fx in fa,fb
这样一共循环两次,fx先后被赋值为fa和fb,不过这明显不是想要的效果。Python里面有一种构造序列的方法是:
[a,b for a in fa for b in fb]
这样构造的序列实际上是等效于两个循环嵌套得到的,循环结构等效于:
for a in fa: for b in fb:
Python里面还有什么东西是能够同时迭代多个序列的吗?想起来有个函数map(function, iterable, ...),它能够同时遍历给定的多个序列,每次都从每个序列中各取一个值组成一个元组对象,然后调用function并传入该对象。如果多个序列的长度不一样,那么所有其他序列都会被用None填充到最长序列的长度。利用map()函数代码可以简化为:
with open("a.txt") as fa, open("b.txt") as fb: map(do_something_with, fa, fb)
看上去真是好极了!但是map()在执行过程中会将每次调用function返回的值添加到一个list中,map()执行完毕以后就会返回这个list。然而这里并不需要这个list,就显得有些浪费了,不能仅仅为了追求代码的简短就放弃效能。但是还有别的办法吗?
其实理论上来说同时迭代多个序列这种功能,一般是不会在语言层面或者是核心库中提供支持的。因为这种行为不够基础和通用,程序员自己完全可以稍微写几行代码来实现,就像前面那样。那么退一步假设,如果真的没办法直接同时迭代多个序列,能不能把两个序列合并成一个二元组序列,然后迭代这个序列呢?想到了一个函数zip([iterable, ...])。正如刚才所说的,zip()能够把多个序列合并成一个新的list,新的list中每个元素都是一个元组对象。跟map()不一样的是,如果多个序列的长度不一样,那么最终返回的序列长度为最短序列的长度。利用zip()代码可以写成这样:
with open("a.txt") as fa, open("b.txt") as fb: for a,b in zip(fa, fb): do_something_with(a, b)
这就是一直想要达到的效果啊!但仔细一分析还是不对,zip()生成了一个新的list,而且这个list的尺寸至少是两个文件的尺寸之和!而这里需要的只是每次从两个文件里面各读取一行,然后作处理,处理完了以后,这两个行占用的空间就可以释放了,然后再继续各自读取下一行,所以这个过程本来是不会太消耗内存的。
这样也不行,还有什么办法呢?在网上找了很久,百度、必应、stackoverflow搜了好几遍,都没有找到满意的答案。就在快要绝望的时候,无意间看到stackoverflow上有人说他的一段代码有问题。只是大概扫了一眼,甚至都没仔细看他的完整描述,只注意到了一个单词——izip!当时脑子里就在想,这是什么?这个跟zip()函数有什么不同么?立马查询Python的文档,然后看到了一句瞬间惊爆眼球的话:
“跟zip()相似,但是返回一个iterator而非list”
有了itertools.izip(*iterable),一切都好说了,代码最终就是:
from itertools import izip with open("a.txt") as fa, open("b.txt") as fb: for a,b in izip(fa, fb): do_something_with(a, b)
探寻Python中如何同时迭代多个iterable对象