本节书摘来自华章社区《编写高质量Python代码的59个有效方法》一书中的第11条:用zip函数同时遍历两个迭代器,作者[美]布雷特·斯拉特金(Brett Slatkin),更多章节内容可以访问云栖社区“华章社区”公众号查看
第11条:用zip函数同时遍历两个迭代器
在编写Python代码时,我们通常要面对很多列表,而这些列表里的对象,可能也是相互关联的。通过列表推导,很容易就能根据某个表达式从源列表推算出一份派生类表(参见本书第7条)。
对于本例中的派生列表和源列表来说,相同索引处的两个元素之间有着关联。如果想平行地迭代这两份列表,那么可以根据names源列表的长度来执行循环。
上面这段代码的问题在于,整个循环语句看上去很乱。用下标来访问names和letters会使代码不易阅读。用循环下标i来访问数组的写法一共出现了两次。改用enumerate来做(参见本书第10条)可以稍稍缓解这个问题,但仍然不够理想。
使用Python内置的zip函数,能够令上述代码变得更为简洁。在Python 3中的zip函数,可以把两个或两个以上的迭代器封装为生成器,以便稍后求值。这种zip生成器,会从每个迭代器中获取该迭代器的下一个值,然后把这些值汇聚成元组(tuple)。与通过下标来访问多份列表的那种写法相比,这种用zip写出来的代码更加明晰。
内置的zip函数有两个问题。
第一个问题是,Python 2中的zip并不是生成器,而是会把开发者所提供的那些迭代器,都平行地遍历一次,在此过程中,它都会把那些迭代器所产生的值汇聚成元组,并把那些元组所构成的列表完整地返回给调用者。这可能会占用大量内存并导致程序崩溃。如果要在Python 2里用zip来遍历数据量非常大的迭代器,那么应该使用itertools内置模块中的izip函数(参见本书第46条)。
第二个问题是,如果输入的迭代器长度不同,那么zip会表现出奇怪的行为。例如,我们又给names里添加了一个名字,但却忘了把这个名字的字母数量更新到letters之中。现在,如果用zip同时遍历这两份列表,那就会产生意外的结果。
新元素'Rosalind'并没有出现在遍历结果中。这正是zip的运作方式。受封装的那些迭代器中,只要有一个耗尽,zip就不再产生元组了。如果待遍历的迭代器长度都相同,那么这种运作方式不会出问题,由列表推导所推算出的派生列表一般都和源列表等长。如果待遍历的迭代器长度不同,那么zip会提前终止,这将会导致意外的结果。若不能确定zip所封装的列表是否等长,则可考虑改用itertools内置模块中的zip_longest函数(此函数在Python 2里叫做izip_longest)。
要点
内置的zip函数可以平行地遍历多个迭代器。
Python 3中的zip相当于生成器,会在遍历过程中逐次产生元组,而Python 2中的zip则是直接把这些元组完全生成好,并一次性地返回整份列表。
如果提供的迭代器长度不等,那么zip就会自动提前终止。
itertools内置模块中的zip_longest函数可以平行地遍历多个迭代器,而不用在乎它们的长度是否相等(参见本书第46条)。