之前已经学过列表解析的基础内容,回顾【迭代器和解析(1)】
下面看一个更高级的列表解析应用
==================================================================
列表解析和矩阵
使用Python编写矩阵(也被称为多维数组)的一个基本方法就是使用嵌套的列表结构。例如,如下代码使用嵌套列表的列表定义了两个3*3的矩阵。
>>> M = [[1,2,3],
[4,5,6],
[7,8,9]]
>>> N = [[2,2,2],
[3,3,3],
[4,4,4]]
使用这样的结构,通常能够使用索引操作:
>>> M[1]
[4, 5, 6]
>>> M[1][2]
6
那么,列表解析也是处理这样结构的强大的工具,因为它会自动扫描行和列。
例如,尽管这种结构通过行存储了矩阵,为了选择第二列,我们能够简单地通过对行进行迭代,之后从所需要的列中提取元素:
>>> [row[1] for row in M]
[2, 5, 8]
>>> [M[row][1] for row in (0,1,2)]
[2, 5, 8]
给出了位置的话,我们能够简单地执行像提取出对角线位置的元素这样的任务。下面的表达式使用range来生成列表的偏移量,并且之后使用相同的行和列来进行索引:
>>> [M[i][i] for i in range(len(M))]
[1, 5, 9]
最后,我们可以使用列表解析来混合多个矩阵。下面的首行代码创建了一个单层的列表,其中包含了矩阵对元素的乘积,然后通过嵌套的列表解析来构建具有相同值的一个嵌套列表结构。
>>> [M[row][col]*N[row][col] for row in range(3) for col in range(3)]
[2, 4, 6, 12, 15, 18, 28, 32, 36]
>>> [[M[row][col]*N[row][col] for col in range(3)] for row in range(3)]
[[2, 4, 6], [12, 15, 18], [28, 32, 36]]
后一个表达式是有效的,因为row迭代是外层的循环。对于每个row,它运行嵌套的列来创建矩阵每一行的结果。它等同于如下的基于语句的代码:
>>> res = []
>>> for row in range(3):
tmp = []
for col in range(3):
tmp.append(M[row][col]*N[row][col])
res.append(tmp)
>>> res
[[2, 4, 6], [12, 15, 18], [28, 32, 36]]
与这些语句相比,列表解析这个版本只需要一行代码,而且可能对于大型矩阵来说,运行相当快。
==================================================================
理解列表解析
如上面的例子,列表解析可能变得难以理解,特别是在嵌套的时候。因此,建议对Python新人,通常使用简单的for循环来处理,在其他大多数情况下,也可以使用map调用。
当然,这样复杂的列表解析具有可观的性能优势:基于对运行在当前Python下的测试,map调用比等效的for循环要快2倍,而列表解析往往比map调用要稍快一些。这些速度上的差距来自于底层的实现上,map和列表解析是在解释器中以C语言的速度来运行的,比Python的for循环代码在PVM中步进要快得多。
-------------------------------------------------------------------------------------------------------
为什么要在意:列表解析和map
回顾文件的readlines方法将返回以换行符\n结束的行:
>>> open('myfile').readlines()
['aaa\n', 'bbb\n', 'ccc\n']
如果不想要换行符,可以使用列表解析或map调用通过一个步骤从所有的行中将它们都去掉。(map的结果在Python3.0中是可迭代的,因此,必须通过list调用来一次性看到其所有结果)
>>> [line.rstrip() for line in open('myfile').readlines()]
['aaa', 'bbb', 'ccc']
>>> [line.rstrip() for line in open('myfile')]
['aaa', 'bbb', 'ccc']
>>> list(map((lambda line:line.rstrip()),open('myfile')))
['aaa', 'bbb', 'ccc']
这里最后两个使用了文件迭代器。
列表解析还能作为一种列选择操作来使用。Python的标准SQL数据库API将返回查询结果保存为与下边类似的元祖的列表:列表就是表,而元祖为行,元祖中的元素就是列的值。
>>> listoftuple = [('Bob',35,'mgr'),('mel',40,'dev')]
一个for循环能够手动从选定的列中提取出所有的值,但是map和列表解析能够一步就做到这一点,并且更快。
>>> [age for (name,age,job)in listoftuple]
[35, 40]
>>> [tup[1] for tup in listoftuple]
[35, 40]
>>> list(map((lambda row:row[1]),listoftuple))
[35, 40]
第一种使用元祖赋值来解包列表中的行元祖,第二种使用索引,第三种也使用索引,不过用到了map和lambda
除了运行函数和表达式之间的区别,Python3.0中的map和列表解析的最大区别是:map是一个迭代器,根据需求产生结果;为了同样的实现内存节省,列表解析必须编码为生成器表达式。所以进入下一主题。
==================================================================
重放迭代器:生成器
现在Python对延迟提供了更多的支持——它提供了工具在需要的时候才产生结果,而不是立即产生结果。特别的,有两种语言结构尽可能地延迟结果创建:
(1)生成器函数:编写为常规的def语句,但是使用yield语句一次返回一个结果,在每个结果之间挂起和继续它们的状态
(2)生成器表达式:类似于列表解析,但是,它们返回按需产生结果的一个对象,而不是构建一个结果列表。
由于二者不会一次性构建一个列表,所以它们节省了内存空间。
-------------------------------------------------------------------------------------------------------
生成器函数:yield VS return
之前学习的常规函数可以接收输入参数并立即送回单个结果,现在,也有可能编写可以送回一个值并随后从其退出的地方继续的函数。这样的函数叫做生成器函数,因为它们随着时间产生值的一个序列。
一般来说,生成器函数和常规函数一样,也是用常规的def语句编写的,然而,当创建时,它们自动实现迭代协议,以便可以出现在需要迭代的环境中。
这里需要再次回顾之前学习的迭代协议:迭代器和解析(1)
-------------------------------------------------------------------------------------------------------
生成函数应用
为了理解生成函数,先来看一个应用。看如下代码,它定义了一个生成器函数,这个函数会用来不断地生成一些列数字的平方
>>> def gensquares(N):
for i in range(N):
yield i**2
这个函数在每次循环时都会产生一个值,之后将其返还给它的调用者。当它被暂停后,它的上一个状态保存了下来,并且在yield语句之后控制器马上被回收。例如,当在一个for循环中时,在循环中每一次完成函数的yield语句之后,控制权都会返还给函数。
>>> for i in gensquares(5):
print(i,end=' ')
0 1 4 9 16
为了终止生成值,函数可以使用一个无值的返回语句,或者在函数主体最后简单地让控制器脱离。
如果想要看看for里面发生了什么,直接调用一个生成器函数:
>>> x = gensquares(4)
>>> x
<generator object gensquares at 0x032B0F30>
得到的是一个生成器对象,它支持迭代器协议,也就是说,生成器对象有一个__next__方法,它可以开始这个函数,或者从它上次yield值后的地方恢复,并且在得到一系列的值的最后一个时,产生StopIteration异常:
>>> next(x)
0
>>> next(x)
1
>>> next(x)
4
>>> next(x)
9
>>> next(x)
Traceback (most recent call last):
File "<pyshell#49>", line 1, in <module>
next(x)
StopIteration
根据我们已知的for循环的工作方式,for循环以同样的方式与生成器一起工作:通过重复调用__next__方法,直到捕获一个异常。如果一个不支持这种协议的对象进行这样的迭代,for循环会使用索引协议进行迭代。
注意在这个例子中,我们能够简单地一次就构建一个所获得的值的列表。
>>> def buildsquares(n):
res = []
for i in range(n):
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 x :x**2,range(5)):
print(x,end = ' ')
0 1 4 9 16
尽管如此,生成器在内存使用和性能方面都更好。它们允许函数避免临时再做所有的工作,当结果的列表很大或者在处理每一个结果都需要很多时间时,这一点尤其有用。
在知道生成函数的应用之后,再看一下它的一些说明。
-------------------------------------------------------------------------------------------------------
状态挂起
和返回一个值并退出的常规函数不同,生成器函数自动在生成值的时刻挂起并继续函数和执行。
由于生成器函数在挂起时保存的状态包含它们的整个本地作用域,当函数恢复时,它们的本地变量保持了信息并且使其可用。
生成器函数和常规函数之间的主要的代码不同之处在于,生成器yield一个值,而不是返回一个值。yield语句挂起该函数并向调用者发送回一个值,但是,保留足够的状态以使得函数能够从它离开的地方继续。当继续时,函数在上一个yield返回后立即继续执行。
从函数的角度看,这允许其代码随着时间产生一系列的值,而不是一次计算它们并在诸如列表的内容中送回它们。
-------------------------------------------------------------------------------------------------------
迭代协议整合
生成函数与迭代协议的概念密切相关。正如我们已经知道的,可迭代对象定义了一个__next__方法,它要么返回迭代中的下一项,或者引发一个特殊的StopIteration异常来终止迭代。一个对象的迭代器用iter内置函数接收。
==================================================================
生成器表达式:迭代器遇到列表解析
从语法上讲,生成器表达式就像一般的列表解析一样,但是它们是括在圆括号而不是方括号中的。
>>> [x**2 for x in range(4)]
[0, 1, 4, 9]
>>> (x**2 for x in range(4))
<generator object <genexpr> at 0x032B0F30>
实际上,编写一个列表解析基本上等同于:在一个list内置调用中包含一个生成器表达式以迫使其一次生成列表中所有的结果。
>>> list((x**2 for x in range(4)))
[0, 1, 4, 9]
尽管如此,从执行过程上讲,生成器表达式很不相同:不是在内存中构建结果,而是返回一个生成器对象,这个对象将会支持迭代协议并在任意的迭代环境中操作。
>>> G = (x**2 for x in range(4))
>>> next(G)
0
>>> next(G)
1
>>> next(G)
4
>>> next(G)
9
>>> next(G)
Traceback (most recent call last):
File "<pyshell#75>", 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
1,0.5
4,2.0
9,4.5
注意,如果生成器表达式是在其他的括号之内,就像在那些函数调用之中,在这种情况下,生成器自身的括号就不是必须的了。尽管这样,在下面第二个sorted调用中,还是需要额外的括号
>>> sum(x**2 for x in range(4))
14
>>> sorted(x**2 for x in range(4))
[0, 1, 4, 9]
>>> sorted((x**2 for x in range(4)),reverse=True)
[9, 4, 1, 0]
>>> import math
>>> list(map(math.sqrt,(x**2 for x in range(4))))
[0.0, 1.0, 2.0, 3.0]
生成器表达式大体上可以认为是对内存空间的优化,它们不需要像方括号的列表解析一样,一次构造出整个结果列表。它们在实际中运行起来可能稍慢一些,所以它们可能只对于非常大的结果集合的运算来说是最优的选择。
==================================================================
生成器函数VS生成器表达式
有趣的是,同样的迭代往往可以用一个生成器函数或一个生成器表达式编写。例如,如下的生成器表达式,把一个字符串中的每个字母重复4次
>>> G = (c*4 for c in 'SPAM')
>>> list(G)
['SSSS', 'PPPP', 'AAAA', 'MMMM']
等价的生成器函数需要略微多一些的代码,但是,作为一个多语句的函数,如果需要的话,它能够编写出更多的逻辑并使用更多的状态信息。
>>> def timesfour(S):
for c in S:
yield c*4
>>> G = timesfour('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 = timesfour('spam')
>>> I = iter(G)
>>> next(I)
'ssss'
>>> next(I)
'pppp'
而实际上,生成器对象本身就是自己的迭代器,有自己的__next__方法,因此无需使用iter来启动迭代。
>>> next(G)
'aaaa'
==================================================================
生成器是单迭代对象
生成器函数和生成器表达式自身都是迭代器,并由此只支持一个活跃迭代。
>>> 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'
>>> next(I1)
'PPPP'
>>> I2 = iter(G)
>>> next(I2)
'AAAA'
此外,一旦任何迭代器运行到完成,所有的迭代器都将用尽,必须产生一个新的生成器以再次开始。
>>> list(I1)
['MMMM']
>>> next(I2)
Traceback (most recent call last):
File "<pyshell#127>", line 1, in <module>
next(I2)
StopIteration
>>> I3 = iter(G)
>>> next(I3)
Traceback (most recent call last):
File "<pyshell#129>", line 1, in <module>
next(I3)
StopIteration
>>> I3 = iter(c*4 for c in 'SPAM')
>>> next(I3)
'SSSS'
对于生成器函数来说,也是如此。
这与某些内置类型的行为不同,比如
迭代器和解析(1)中介绍的range和list都是支持多个迭代器的,这些迭代器会记住它们各自的位置。
>>> L = [1,2,3,4]
>>> L1 = iter(L)
>>> L2 = iter(L)
>>> next(L1)
1
>>> next(L2)
1
>>> next(L2)
2
而zip,map和filter不支持相同结果上的多个活跃迭代器,它们与生成器函数一样。