8.13 生成器表达式
生成器表达式是列表解析的一个扩展。在Python 2.0中我们加入了列表解析,使语言有了一次革命化的发展,提供给用户了一个强大的工具,只用一行代码就可以创建包含特定内容的列表。你可以去问一个有多年Python经验的程序员是什么改变了他们编写Python程序的方式,那么得到最多的答案一定会是列表解析。
另一个在Python版本2.2时被加入的重要特性是生成器。生成器是特定的函数,允许你返回一个值,然后“暂停”代码的执行,稍后恢复。我们将在第11章中讨论生成器。
列表解析的一个不足就是必要生成所有的数据,用以创建整个列表。这可能对有大量数据的迭代器有负面效应。生成器表达式通过结合列表解析和生成器解决了这个问题。
生成器表达式在Python 2.4被引入,它与列表解析非常相似,而且它们的基本语法基本相同;不过它并不真正创建数字列表,而是返回一个生成器,这个生成器在每次计算出一个条目后,把这个条目“产生”(yield)出来。生成器表达式使用了 “延迟计算”(lazy evaluation),所以它在使用内存上更有效。我们来看看它和列表解析到底有多相似:
列表解析:
生成器表达式:
生成器并不会让列表解析废弃,它只是一个内存使用更友好的结构,基于此,有很多使用生成器地方。下面我们提供了一些使用生成器表达式的例子,最后举一个冗长的样例,从它你可以感觉到Python代码在这些年来的变化。
1. 磁盘文件样例
在前边列表解析一节,我们计算文本文件中非空字符总和。最后的代码中,我们展示了如何使用一行列表解析代码做所有的事。如果这个文件的大小变得很大,那么这行代码的内存性能会很低,因为我们要创建一个很长的列表用于存放单词的长度。
为了避免创建庞大的列表,我们可以使用生成器表达式来完成求和操作。它会计算每个单词的长度然后传递给sum()函数(它的参数不仅可以是列表,还可以是可迭代对象,比如生成器表达式)。这样,我们可以得到优化后的代码(代码长度,还有执行效率都很高效):
我们所做的只是把方括号删除:少了两字节,而且更节省内存…非常地环保!
2. 交叉配对样例
生成器表达式就好像是懒惰的列表解析(这反而成了它主要的优势)。它还可以用来处理其他列表或生成器,例如这里的rows和cols:
不需要创建新的列表,直接就可以创建配对。我们可以使用下面的生成器表达式:
现在我们可以循环x_product_pairs,它会懒惰地循环rows和cols:
3. 重构样例
我们通过一个寻找文件最长的行的例子来看看如何改进代码。在以前,我们这样读取文件:
事实上,这还不够老。真正的旧版本Python代码中,布尔常量应该写是整型1,而且我们应该使用string模块而不是字符串的strip()方法:
从那时起,我们认识到如果读取了所有的行,那么应该尽早释放文件资源。如果这是一个很多进程都要用到的日志文件,那么理所当然我们不能一直拿着它的句柄不释放。是的,我们的例子是用来展示的,但是你应该得到这个理念。所以读取文件的行的首选方法应该是这样:
列表解析允许我们稍微简化代码,而且我们可以在得到行的集合前做一定的处理。在下段代码中,除了读取文件中的行之外,我们还调用了字符串的strip()方法处理行内容。
然而,两个例子在处理大文件时候都有问题,因为readlines()会读取文件的所有行。后来我们有了迭代器,文件本身就成为了它自己的迭代器,不需要调用readlines()函数。我们已经做到了这一步,为什么不去直接获得行长度的集合呢(之前我们得到的是行的集合)?这样,我们就可以使用max()内建函数得到最长的字符串长度:
这里唯一的问题就是你一行一行迭代f的时候,列表解析需要文件的所有行读取到内存中,然后生成列表。我们可以进一步简化代码:使用生成器表达式替换列表解析,然后把它移到max()函数里,这样,所有的核心部分只有一行:
最后,我们可以去掉文件打开模式(默认为读取),然后让Python去处理打开的文件。当然,文件用于写入的时候不能这么做,但这里我们不需要考虑太多:
我们走了好长一段路。注意,即便是这只有一行的Python程序也不是很晦涩。生成器表达式在 Python 2.4中被加入,你可以在PEP 289中找到更多相关内容。