个人总结难免疏漏,请多包涵。更多内容请查看原文。本文以及学习笔记系列仅用于个人学习、研究交流。
本文会回顾迭代、列表解析、map,新学习生成器函数及其相关的生成器函数、表达式——这是用户定义的、按需产生结果的方式。较简单。
目录
回顾列表解析:函数式编程工具
列表解析与map
增加测试和嵌套循环
列表解析和矩阵
理解列表解析
为什么在意:列表解析和map
重访迭代器:生成器
生成器函数:yield VS return
状态挂起
迭代协议整合
生成器函数应用
生成器表达式
生成器函数VS生成器表达式
生成器是单迭代器对象
迭代工具模拟zip和map
编写自己的map(func,...)
编写自己的zip(...)
为什么留意:单次迭代
内置类型和类中的值生成
回顾列表解析:函数式编程工具
上一章中学习了map和filter这样的函数式编程工具,它们将操作映射到序列和集合结果中。由于这是Python编程中的一种常见任务,Python最终产生了一种新的表达式——列表解析,它甚至比我们前面学习的工具更灵活。
简而言之,列表解析把任意一个表达式而不是一个函数应用于一个迭代对象中的元素。同样,它可以是更为通用的工具。
在第14笔记学习循环语句时介绍了列表解析。但是因为它们与map和filter这样的函数式编程工具相关,所以将继续回顾这一话题的内容。从技术上讲,这个特性并没有与函数绑定在一起。正如我们所见到的,列表解析可以成为一个比map和filter更通用的工具,有时候通过基于函数的另类视角进行分析,有助于深入理解它。
列表解析与map
举一个例子来说明。Python的内置ord函数会返回一个单个字符的ASCII整数编码(chr内置函数是它的逆过程,它将一个ASCII整数编码转换为字符):
现在,假设希望收集整个字符串中的所有字符的ASCII编码。也许最直接的方法就是使用一个简单的for循环,并将结果附加在列表中:
然而现在我们知道了map,我们能够使用一个单个的函数调用,而不必关心代码中列表的结构,从而实现起来更简单:
尽管如此,我们能够通过列表解析表达式得到相同的结果——map把一个函数映射遍一个序列,列表解析把一个表达式映射遍一个序列:
列表解析在一个序列的值上应用一个任意表达式,将其结果收集到一个新的列表中并返回。从语法上来说,列表解析是由方括号封装起来的(为了提醒你它们构造了一个列表)。它们的简单形式是在方括号中编写一个表达式,其中的变量,在后边跟随着的看起来就像一个for循环的头部一样的语句,有着相同的变量名的变量。Python之后将这个表达式的应用循环中每次迭代的结果收集起来。
上一个例子的效果与手动进行for循环和map调用相比,没有什么不同。然而,列表解析可以变得更方便,当我们希望对一个序列应用一个任意表达式的时候。
这里收集了从0~9数字的平方。和map调用差不多,我们也能够创建一个小函数来实现平方操作。因为在其他的地方不需要这个函数,通常(但不是必须)在行内编写,使用lambda,而不是使用其他地方的def语句:
这同样也有效,并且它要比等效的列表解析编写更少的代码。它只是稍有一点复杂(至少,一旦理解了lambda后)。对于更高级种类的表达式,通常列表解析将会被认为是输入较少的。
增加测试和嵌套循环
列表解析甚至要比现在所介绍的更通用。例如,可以在for之后编写一个if分支,用来增加选择逻辑。使用了if分支的列表解析能够当成一种与上一部分讨论过的内置的filter类似的工具,它们会在分支不是真的情况下跳过一些序列的元素。
举一个选择出从0~4的偶数的例子。像刚刚看到过的map可以替代列表解析,为了测试表达式,这里的filter版本创建了一个小的lambda函数。为了对比,在这里也显示了等效的for循环。
所有的这些都是用了求余(求除法的余数)操作符%,用来检测该数是否是偶数。如果一个数字除以2以后没有余数,它就一定是偶数。filter调用与这里的列表解析相比也更短。
尽管如此,在列表解析中能够混合一个if分支以及任意的表达式,从而赋予了它通过一个单个表达式,完成了一个filter和一个map相同的功效。
这次,收集了从0~9的偶数的平方。若在右边的if中得到的是假的话,for循环就会跳过这些数字,并且用左边的表达式来计算值。这个等效的map调用将需要更多的工作来完成这一部分。我们需要在map迭代中混合filter选择过程,这使得表达式明显复杂得多。
实际上,列表解析还能够更加通用。你可以在一个列表解析中编写任意数量的嵌套的for循环,并且每一个都有可选的关联的if测试。
通用的列表解析的结构如下所示。
当for分句嵌套在列表解析中时,它们工作起来就像等效的嵌套的for循环语句。例如,如下代码。
与下文如此冗长的代码有相同的效果。
尽管列表解析创建了列表,记住它们能够像任意的序列和其他迭代类型一样进行迭代。这里有个小巧简单的代码,能够不使用列表的数字索引遍历字符串,并收集它们合并后的结果。
最后,这里有个复杂得多的列表解析工具,表明了在嵌套的for从句中附加if选择的作用。
这个表达式排列了从0~4的偶数与从0~4的奇数的组合。其中if分句过滤出了每个序列中需要进行迭代的元素。这里是一个等效的用语句编写而成的代码:
注意,如果你对一个复杂的列表解析有什么困惑的话,你总是能够将列表解析的for和if分句在其中进行嵌套(将后来的分句缩进到右边),从而得到等效的语句。得到的结果要长得多,但是也许更清晰。
而map和filter的等效形式往往将会更复杂也会有深层的嵌套。
列表解析和矩阵
看一个更高级的列表解析应用,来进一步学习。使用Python编写矩阵(也被称为多维数组)的一个基本的方法就是使用嵌套的列表结构。例如,如下代码使用嵌套列表的列表定义了两个3×3的矩阵。
使用这样的结构,我们总是能够索引行,以及索引行中的列,使用通常的索引操作。
列表解析也是处理这样结构的强大的工具,因为它将会自动为我们扫描行和列。例如,尽管这种结构通过行存储了矩阵,为了选择第二列,能够简单地通过对行进行迭代,之后从所需要的列中提取出元素,或者就像下面一样通过在行内的位置进行迭代。
给出了位置的话,我们能够简单地执行像提取出对角线位置的元素这样的任务。下面的表达式使用range来生成列表的偏移量,并且之后使用相同的行和列来进行索引,取出了M[0][0],之后是M[1][1](我们假设矩阵有相同数目的行和列)。
最后,我们使用列表解析来混合多个矩阵。下面的首行代码创建了一个单层的列表,其中包含了矩阵对元素的乘积,然后通过嵌套的列表解析来构建具有相同值的一个嵌套列表结构。
最后一个表达式是有效的,因为row迭代是外层的循环。对于每个row,它运行嵌套的列的迭代来创建矩阵每一行的结果。它等同于如下的基于语句的代码。
与这些语句相比,列表解析这个版本只需要一行代码,而且可能对于大型矩阵来说,运行相当快。
理解列表解析
有了这样的通用性,列表解析变得难以理解,特别是在嵌套的时候。因此,建议对于刚开始使用Python的编程者,通常使用简单的for循环,在其他大多数情况下,使用map调用(除非它们会变得过于复杂)。“保持简洁”法则就在这里生效了,就像往常一样:实现代码的精简与代码的可读性相比,就没有那么重要了。
尽管如此,在这种情况下,对当前额外的复杂度来说有可观的性能优势:基于对运行在当前Python下的测试,map调用比等效的for循环要快两倍,而列表解析往往比map调用要稍快一些。
要自行测试这些方案的速度,可以参考标准库time模块的time.clock和time.time调用
速度上的差距是来自于底层实现上,map和列表解析是在解释器中以C语言的速度来运行的,比Python的for循环代码在PVM中步进运行要快得多。
因为for循环让逻辑变得更清晰,基于简单性我们通常推荐使用。尽管如此,map和列表解析作为一种简单的迭代是容易理解和使用的,而且如果应用对速度特别重视的话。
此外,因为map和列表解析都是表达式,从语法上来说,它们能够在for循环语句不能够出现的地方使用。例如,在一个lambda函数的主体中或者是在一个列表或字典常量中。然而应该尝试让map调用和列表解析保持简单。对于更复杂的任务,用完整的语句来替代。
为什么在意:列表解析和map
介绍一个实际应用中更现实的列表和map的例子,回顾文件的readlines方法将返回以换行符\n结束的行:
如果不想要换行符,可以使用列表解析或map调用通过一个步骤从所有的行中将它们都去掉:
这里最后两个使用了文件迭代器(这里实际上是指不需要一个方法调用就能够在迭代中获取所有的行)。map调用要比列表解析稍长一些,但是无论哪种方法都没有必要明确地管理结果列表的构造。
列表解析还能作为一种列选择操作来使用。Python的标准SQL数据库API将返回查询结果保存为与下边类似的元组的列表:列表就是表,而元组为行,元组中的元素就是列的值:
一个for循环能够手动从选定的列中提取出所有的值,但是map和列表解析能够一步就做到这一点,并且更快。
第一种方法使用元组赋值来解包列表中的行元组,第二种方法使用索引。
更多关于Python的数据库API请参考其他的书籍和资源。
除了运行函数和表达式之间的区别,Python 3.0中的map和列表解析的最大区别是:map是一个迭代器,根据需求产生结果;为了同样地实现内存节省,列表解析必须编码为生成器表达式(本章的主题之一)。
重访迭代器:生成器
如今Python对延迟提供更多的支持——它提供了工具在需要的时候才产生结果,而不是立即产生结果。特别地,有两种语言结构尽可能地延迟结果创建。
- 生成器函数:编写为常规的def语句,但是使用yield语句一次返回一个结果,在每个结果之间挂起和继续它们的状态。
- 生成器表达式类似于上一小节的列表解析,但是,它们返回按需产生结果的一个对象,而不是构建一个结果列表。
二者都不会一次性构建一个列表,它们节省了内存空间,并且允许计算时间分散到各个结果请求。
二者最终都通过实现第14笔记所介绍的迭代协议来执行它们延迟结果。
生成器函数:yield VS return
之前已经学习了编写接收输入参数并立即送回单个结果的常规函数。然而,也有可能来编写可以送回一个值并随后从其退出的地方继续的函数。这样的函数叫做生成器函数,因为它们随着时间产生值的一个序列。
一般来说,生成器函数和常规函数一样,并且,实际上也是用常规的def语句编写的。然而,当创建时,它们自动实现迭代协议,以便可以出现在迭代背景中。这里再次回顾迭代器,看看是如何与生成器相关的。
状态挂起
和返回一个值并退出的常规函数不同,生成器函数自动在生成值的时刻挂起并继续函数的执行。因此,它们对于提前计算整个一系列值以及在类中手动保存和恢复状态都很有用。
由于生成器函数在挂起时保存的状态包含它们的整个本地作用域,当函数恢复时,它们的本地变量保持了信息并且使其可用。
生成器函数和常规函数之间的主要的代码不同之处在于,生成器yields一个值,而不是返回一个值。yield语句挂起该函数并向调用者发送回一个值,但是,保留足够的状态以使得函数能够从它离开的地方继续。当继续时,函数在上一个yield返回后立即继续执行。从函数的角度来看,这允许其代码随着时间产生一系列的值,而不是一次计算它们并在诸如列表的内容中送回它们。
迭代协议整合
要真正地理解生成器函数,要知道,它们与Python中的迭代协议的概念密切相关。
可迭代的对象定义了一个__next__方法,它要么返回迭代中的下一项,或者引发一个特殊的StopIteration异常来终止迭代。一个对象的迭代器用iter内置函数接收。
如果支持该协议的话,Python的for循环以及其他的迭代背景,使用这种迭代协议来遍历一个序列或值生成器;如果不支持,迭代返回去重复索引序列。
要支持这一协议,函数包含一条yield语句,该语句特别编译为生成器。当调用时,它们返回一个迭代器对象,该对象支持用一个名为__next__的自动创建的方法来继续执行的接口。生成器函数也可能有一条return语句,总是在def语句块的末尾,直接终止值的生成。从技术上讲,可以在任何常规函数退出执行之后,引发一个StopIteration异常来现实。从调用者的角度来看,生成器的__next__方法继续函数并且运行到下一个yield结果返回或引发一个StopIteration异常。
直接效果就是生成器函数,编写为包含yield语句的def语句,自动地支持迭代协议,并且由此可能用在任何迭代环境中以随着时间并根据需要产生结果。
生成器函数应用
看如下代码,它定义了一个生成器函数,这个函数将会用来不断地生成一系列的数字的平方。
def gensquares(b):
for i in range(b): yield i **2
这个函数在每次循环时都会产生一个值,之后将其返还给它的调用者。当它被暂停后,它的上一个状态保存了下来,并且在yield语句之后控制器马上被回收。例如,当用在一个for循环中时,在循环中每一次完成函数的yield语句后,控制权都会返还给函数。
for i in gensquares(5):
print(i,end=' ')
0 1 4 9 16
为了终止生成值,函数可以使用一个无值的返回语句,或者在函数主体最后简单地让控制器脱离。
如果想要看看在for里面发生了什么,直接调用一个生成器函数:
x = gensquares(5)
x
<generator object gensquares at 0x000001DA221F98A0>
得到的是一个生成器对象,它支持迭代器协议。就是说,生成器对象有一个__next__方法,它可以开始这个函数,或者从它上次yield值后的地方恢复,并且在得到一系列的值的最后一个时,产生StopIteration异常。为了方便起见,next(X)内置函数为我们调用一个对象的X.__next__()方法:
next(x)
0
next(x)
1
next(x)
4
next(x)
9
next(x)
16
next(x)
Traceback (most recent call last):
File "<pyshell#16>", line 1, in <module>
next(x)
StopIteration
for循环(以及其他的迭代环境)以同样的方式与生成器一起工作:通过重复调用__next__方法,直到捕获一个异常。如果一个不支持这种协议的对象进行这样迭代,for循环会使用索引协议进行迭代。__next__方法应用于所有的可迭代对象(包括内置类型和用户定义的类)。
注意在这个例子中能够简单地一次就构建一个所获得的值的列表
def buildsquares(b):
res = []
for i in range(b): res.append(i ** 2)
return res
for x in buildsquares(5):print(x,end=' ')
0 1 4 9 16
还能够使用for循环、map或者列表解析的技术来实现:
for x in [n ** 2 for n in range(5)]: print(x,end=' ')
0 1 4 9 16
for x in map((lambda n: n**2 ),range(5)):
print(x,end=' ')
0 1 4 9 16
尽管如此,生成器在内存使用和性能方面都更好。它们允许函数避免临时再做所有的工作,当结果的列表很大或者在处理每一个结果都需要很多时间时,这一点尤其有用。生成器将在loop迭代中处理一系列值的时间分布开来。
尽管如此,对于更多高级的应用,它们提供了一个更简单的替代方案来手动将类的对象保存到迭代中的状态。
有了生成器,函数变量就能进行自动的保存和恢复。
生成器表达式
迭代器和列表解析的概念形成了这种语言的一个新的特性,生成器表达式。
从语法上来讲,生成器表达式就像一般的列表解析一样,但是它们是括在圆括号中而不是方括号中的。
实际上,至少在一个函数的基础上,编写一个列表解析基本上等同于:在一个list内置调用中包含一个生成器表达式以迫使其一次生成列表中所有的结果。
从执行过程上来讲,生成器表达式很不相同:不是在内存中构建结果,而是返回一个生成器对象,这个对象将会支持迭代协议并在任意的迭代语境的操作中。
G = (x**2 for x in range(3))
next(G)
0
next(G)
1
next(G)
4
next(G)
Traceback (most recent call last):
File "<pyshell#37>", line 1, in <module>
next(G)
StopIteration
一般不会机械地使用next迭代器来操作生成器表达式,因为for循环会自动触发。
for num in(x**2 for x in range(4)):
print('%s,%s'%(num,num/2.0))
0,0.0
1,0.5
4,2.0
9,4.5
实际上,每一个迭代的语境都会这样,包括sum、map和sorted等内置函数,以及其他迭代语境,例如any、all和list内置函数等。
注意,如果生成器表达式是在其他的括号之内,就像在那些函数调用之中,在这种情况下,生成器自身的括号就不是必须的了。
尽管这样,在下面第二个sorted调用中,还是需要额外的括号。
生成器表达式大体上可以认为是对内存空间的优化,它们不需要像方括号的列表解析一样,一次构造出整个结果列表。
它们在实际中运行起来可能稍慢一些,所以它们可能只对于非常大的结果集合的运算来说是最优的选择。关于性能的更权威的评价,等到稍后编写计时脚本的时候给出。
生成器函数VS生成器表达式
同样的迭代往往可以用一个生成器函数或一个生成器表达式编写。
等价的生成器函数需要略微多一些的代码,但是,作为一个多语句的函数,如果需要的话,它将能够编写更多的逻辑并使用更多的状态信息。
def timefour(s):
for c in s: yield c *4
G = timefour('spam')
list(G)
['ssss', 'pppp', 'aaaa', 'mmmm']
表达式和函数支持自动迭代和手动迭代——前面的列表自动调用迭代,如下的迭代手动进行。
G = (c*4 for c in'SPAM')
i = iter(G)
next(i)
'SSSS'
next(i)
'PPPP'
next(i)
'AAAA'
next(i)
'MMMM'
G = timefour('spam')
i = iter(G)
next(i)
'ssss'
next(i)
'pppp'
next(i)
'aaaa'
next(i)
'mmmm'
这里可以看出生成器是单次迭代器。
生成器是单迭代器对象
生产器函数和生成器表达式自身都是迭代器,并由此只支持一次活跃迭代——不像一些内置类型,无法有在结果集中位于不同位置的多个迭代器。
例如,一个生成器的迭代器是生成器自身(实际上,在一个生成器上调用iter没有任何效果)。
G = (c*4 for c in'SPAM')
iter(G) is G
True
手动地使用多个迭代器来迭代结果流,它们将会指向相同的位置。
G = (c*4 for c in'SPAM')
i1 = iter(G)
next(i1)
'SSSS'
i2 = iter(G)
next(i2)
'PPPP'
一旦任何迭代器运行到完成,所有的迭代器都将用尽,必须产生一个新的生成器以再次开始。
对于生成器函数来说,也是如此,如下的基于语句的def等价形式只支持一个活跃的生成器并且在一次迭代之后用尽。
这与某些内置类型的行为不同,它们支持多个迭代器并且在一个活动迭代器中传递并反映它们的原处修改。列表支持多个迭代器。
迭代工具模拟zip和map
了解了列表解析、生成器和其他的迭代工具,就知道模拟众多的Python的函数式内置工具既直接又很有益。
已经看到了内置的zip和map函数如何组合可迭代对象和映射函数。使用多个序列参数,map以与zip配对元素相同的方式,把函数映射到取自每个序列的元素。
尽管它们用于不同的目的,可能会注意到zip结果和执行map的函数参数之间的一种关系,下面的例子可以说明这种关系。
编写自己的map(func,...)
尽管map和zip内置函数快速而方便,总是可以在自己的代码中模拟它们。
例如,在上一章中,看到一个函数针对单个的序列参数来模拟map内置函数。针对多个序列的时候,也并不会费太多工夫就可以像内置函数那样操作。
这个版本很大程度上依赖于特殊的*args参数传递语法。它收集多个序列(实际上,是可迭代对象)参数,将其作为zip参数解包以便组合,然后成对的zip结果解包作为参数以便传入到函数。也就是说,在使用这样的一个事实,zip是map中的一个基本的嵌套操作。最后的测试代码对一个序列和两个序列都应用了这个函数,以产生这一输入(可以用内置的map得到同样的输出)。
实际上,前面的版本展示了经典的列表解析模式,在一个for循环中构建操作结果的一个列表。
可以更精简地编写自己的map,作为单行列表解析的对等体。
def mymap(func,*seqs):
return [func(*args) for args in zip(*seqs)]
print(mymap(abs, [-2,-1,0,1,2]))
[2, 1, 0, 1, 2]
print(mymap(pow, [1,2,3],[2,3,4,5]))
[1, 8, 81]
当这段代码运行的时候,结果与前面相同,但是,这段代码更加精炼并且可能运行的更快。
之前的mymap版本一次性构建结果列表,并且对于较大的列表来说,这可能浪费内存。既然知道了生成器函数和表达式,重新编码这两种替代方案来根据需求产生结果是很容易的。
def mymap(func,*seqs):
res=[]
for args in zip(*seqs):
yield func(*args)
def mymap(func,*seqs):
return (func(*args) for args in zip(*seqs))
这些版本产生同样的结果,但是返回设计用来支持迭代协议的生成器。第一个版本每次yield一个结果,第二个版本返回一个生成器表达式的结果来做同样的事情。如果把它们包含到一个list调用中迫使它们一次生成所有的值,它们会产生同样的结果。
print(list(mymap(abs, [-2,-1,0,1,2])))
[2, 1, 0, 1, 2]
print(list(mymap(pow, [1,2,3],[2,3,4,5])))
[1, 8, 81]
这里并没有做什么实际工作,直到list调用迫使生成器运行,通过激活迭代协议而进行。生成器由这些函数自身返回,也由它们所使用的zip内置函数返回,根据需要产生结果。
编写自己的zip(...)
目前给出的示例中的很多魔力在于,它们使用zip内置函数来配对来自多个序列的参数。
def myzip(*seqs):
seqs = [list(s) for s in seqs]
res = []
while all(seqs):
res.append(tuple(s.pop(0) for s in seqs))
return res
s1,s2 = 'abc','xyz123'
print(myzip(s1,s2))
[('a', 'x'), ('b', 'y'), ('c', 'z')]
这里编写的函数可以在任何类型的可迭代对象上运行,因为它们通过list内置函数来运行自己的参数以迫使结果生成(例如,文件像参数一样工作,此外,序列像字符串一样)。
注意这里的all内置函数的使用,如果一个可迭代对象中的所有或任何元素为True(或者对等的为非空),它们分别返回True。当列表中的任何或所有参数在删除后变成了空,这些内置函数将用来停止循环。
zip构建并返回列表,用yield将它们转换为生成器以便它们每个都是每次返回结果中的一项,这还是很容易做到的。结果和前面的相同,但是,需要再次使用list来迫使该生成器产生其值以供显示。
def myzip(*seqs):
seqs = [list(s) for s in seqs]
while all(seqs):
yield tuple(s.pop(0) for s in seqs)
print(myzip(s1,s2)) #直接打印是一个对象
<generator object myzip at 0x000001DA21CCCAC0>
print(list(myzip(s1,s2)))
[('a', 'x'), ('b', 'y'), ('c', 'z')]
还有别的替代实现,不是使用pop方法从列表中删除参数,而是通过计算最小和最大参数长度来完成其工作。有了这些长度,很容易编写嵌套的列表解析来遍历参数索引范围。
def myzip(*seqs):
minlen = min(len(s) for s in seqs)
return [tuple(s[i] for s in seqs) for i in range(minlen)]
print(myzip(s1,s2))
[('a', 'x'), ('b', 'y'), ('c', 'z')]
这些代码使用len和索引,它们假设参数是序列或类似的,而不是任意的可迭代对象。这里,外围的解析遍历参数索引范围,内部的解析(传递到元组)遍历传入的序列以并列地提取参数。当它们运行时,结果和前面相同。
生成器和迭代器似乎在这个例子中泛滥。传递给min的参数是生成器表达式,它在嵌套的解析开始迭代之前运行完成。此外,嵌套的列表解析使用了两个层级的延迟计算——range内置函数是一个可迭代对象,就像生成器表达式参数对元组。
实际上,这里没有产生结果,直到列表解析的方括号要求放入到结果列表中的值——它们迫使解析和生成器运行。为了把这些函数自身转换为生成器而不是列表构建器,使用圆括号而不是方括号。zip的例子如下所示。
def myzip(*seqs):
minlen = min(len(s) for s in seqs)
return (tuple(s[i] for s in seqs) for i in range(minlen))
print(list(myzip(s1,s2)))
[('a', 'x'), ('b', 'y'), ('c', 'z')]
它用一个list调用来激活生成器和迭代器以产生它们自己的结果。
为什么留意:单次迭代
之前看到了一些内置函数(如map)如何只支持一个单个的便利,并且在发生之后为空,提过会给出一个示例展示这在实际中是如何变得微妙而重要的。看下面的例子
由于这段代码使用iter和next,它对任何类型的可迭代对象都有效。注意,当这个参数的迭代器之一用尽时,没有任何理由捕获由这个解析内的next(i t)来引发的StopIteration——允许它传递会终止这个生成器函数,并且与一条return语句具有相同的效果。如果至少传递了一个参数的话,while iters:对于循环来说足够了,并且,避免了无限循环(列表解析将总是返回一个空的列表):
在Python3.0后,它陷入了一个无限循环中并失效,因为Python 3.0的map返回一个单次可迭代对象。在Python 3.0中,只要在循环中运行了一次列表解析,iters将会永远为空(并且res将会是[])。
为了使其在Python 3.0下正常工作,需要使用list内置函数来创建一个支持多次迭代的对象:
Python 3.0中把map调用放入到list调用中不仅是为了显示。
内置类型和类中的值生成
很多内置的类型以类似的方式工作,例如,字典拥有在每次迭代中产生键的迭代器。
和手动编写的生成器所产生的值一样,字典键也可以手动迭代,或者使用包括for循环、map调用、列表解析等等自动迭代工具。
在文件迭代器中,Python简单地载入了一个文件的行。
尽管内置类型迭代器绑定到了一个特定类型的值生成,概念与使用表达式和函数编写的生成器是类似的。像for循环这样的迭代环境接受任何的可迭代对象,不管是用户定义的还是内置的。
可能用遵守迭代协议的类来实现任意的用户定义的生成器对象。这样的类定义了一个特别的__iter__方法,它由内置的iter函数调用,将返回一个对象,该对象有一个__next__方法,该方法由next内置函数调用(一个__getitem__索引方法作为迭代的退而求其次的选项也是可以的)。
从这样一个类创建的实例对象,看做是可迭代的,并且可以用在for循环和所有其他的迭代环境中。然而,有了类,我们可以访问比其他生成器构造所能提供的更丰富的逻辑和数据结构选项。迭代器的内容不会真正结束,直到我们了解到它如何映射到类。这个话题到第29章学习基于类的迭代器进行详细介绍。