举例讲解Python中的迭代器、生成器与列表解析用法

时间:2022-11-19 10:31:53

迭代器:初探

上一章曾经提到过,其实for循环是可用于任何可迭代的对象上的。实际上,对Python中所有会从左至右扫描对象的迭代工具而言都是如此,这些迭代工具包括了for循环、列表解析、in成员关系测试以及map内置函数等。

“可迭代对象”的概念在Python中是相当新颖的,基本这就是序列观念的通用化:如果对象时实际保存的序列,或者可以再迭代工具环境中一次产生一个结果的对象,那就看做是可迭代的。

>>文件迭代器
作为内置数据类型的文件也是可迭代的,它有一个名为__next__的方法,每次调用时,就会返回文件中的下一行。当到达文件末尾时,__next__会引发内置的StopIteration异常,而不是返回空字符串。

这个接口就是Python中所谓的迭代协议:有__next__方法的对象会前进到下一个结果,而在一系列结果的末尾时,则会引发StopIteration。任何这类对象都认为是可迭代的。任何这类对象也能以for循环或其他迭代工具遍历,因为所有迭代工具内部工作起来都是在每次迭代中调用__next__,并且捕捉StopIteratin异常来确定何时离开。

for line in open('script.py'):
 print(line.upper(),end='')

上面的代码就是文件迭代的一个例子,并且这种用法是最高效的文件读取方法,主要有三个优点:这是最简单的写法,运行快,并且从内存使用情况来说也是最好的。

替代的写法是:

for line in open('script.py').readlines():
 print(line.upper(),end='')

这种调用方法会把文件一次性读到内存中,如果文件太大,那么内存会被消耗光的。

>>手动迭代:iter和next
为了支持手动迭代代码(用较少的录入),Python3.0还提供了一个内置函数next,它会自动调用一个对象的__next__方法。给定一个对象X,调用next(X)等同于X.__next__(),但前者简单很多。

从技术角度来讲,迭代协议还有一点值得注意。当for循环开始时,会通过它传给iter内置函数,以便从可迭代对象中获得一个迭代器,返回的对象含有需要的next方法。调用iter的步骤对于文件来说不是必须的,因为文件对象就是自己的迭代器,但是对于其他的一些内置数据类型来说,就不一定了。

列表以及很多其他的内置对象,不是自身的迭代器,因为它们支持多次打开迭代器。对这样的对象,我们必须调用iter来启动迭代:

L=[1,2,3]
iter(L) is L #return false
L.__next__() #会报错

I = iter(L)
I.__next__()
I.__next__()

虽然Python迭代工具自动调用这些(iter,__next__)函数,我们也可以使用它们来手动地应用迭代协议。

列表解析:初探

>>列表解析基础知识

L=[1,2,3,4,5]
L = [x+10 for x in L]

列表解析写在一个方括号中,因为它们最终是构建一个新的列表的一种方式。它们以我们所组成的一个任意的表达式开始,该表达式使用我们所组成的一个循环变量(x+10)。这后面跟着我们现在应该看作是一个for循环头部的部分,它申明了循环变量,以及一个可迭代对象(for x in L)

要运行该表达式,Python在解释器内部执行一个遍历L的迭代,按照顺序把x赋给每个元素,并且收集对各元素运行左边的表达式的结果。我们得到的结果列表就是列表解析所表达的内容——针对L中的每个x,包含了x+10的一个新列表。

其实列表解析式并不是必须的,因为它能完成的工作都能够通过for循环完成,但是列表解析式比手动的for循环语句运行得更快(往往速度快一倍),因为它们的迭代在解释器内部是以C语言的速度执行的,而不是以手动的Python代码执行的,特别是对于较大的数据集合,这是使用列表解析的一个主要的性能优点。

当我们考虑在一个序列中的每个项上执行一个操作时,都可以考虑使用列表解析。

>>扩展的列表解析语法
实际上,列表解析可以有更高级的应用。作为一个特别有用的扩展,表达式中嵌套的for循环可以有一个相关的if子句,来过滤那些测试不为真的结果项。

lines = [line.rstrip() for line in open('script.py') if line[0]='p']

这条if子句检查从文件读取的每一行,看它的第一个字符是否是p;如果不是,从结果列表中省略改行。

事实上,如果我们愿意的话,列表解析可以变得更加复杂——它们的完整语法允许任意数目的for子句,每个子句有一个可选的相关的if子句。

Python3.0中的新的可迭代对象

Pyton3.0中的一个基本的改变是,它比Python2.x更强调迭代。除了与文件和字典这样的内置类型相关的迭代,字典方法keys、values和items都在Python3.0中返回可迭代对象。 返回一个可迭代对象而不是返回一个结果列表的好处在于节省了内存的空间。

>>多个迭代器VS单个迭代器
多个迭代器:在它们的结果中能保持不同位置的多个迭代器
单个迭代器:只能保持一个迭代器,在遍历其结果之后,它们就用尽了。
通常通过针对iter调用返回一个新的对象,来支持多个迭代器;单个迭代器一般意味着一个对象返回其自身。

>>字典视图迭代器
在Python3.0中,字典的keys、values和items方法返回可迭代的视图对象,它们一次产生一个结果项,而不是在内存中一次产生全部结果列表。视图项保持和字典中的那些项相同的物理顺序,并且反映对底层的字典做出的修改。

和所有迭代器一样,我们总可以通过把一个Python3.0字典视图传递到list内置函数中,从而强制构建一个真正的列表。然而,这通常不是必须的。

此外,Python3.0字典仍然有自己的迭代器,它返回连续的键。因此,无需直接在此环境中调用keys:

for key in D:print(key,end='')

>>列表解析与map
列表解析在一个序列上的值应用一个任意表达式,将其结果搜集到一个新的列表中并返回。从语法上说,列表解析是由方括号封装起来的(为了提醒你它们构造了一个列表)。它们的简单形式是在方括号中编写一个表达式,Python之后将这个表达式的应用循环中每次迭代的结果搜集起来。例如,假如我们希望搜集字符串中的所有字符的ASCII码,可以这样做:

#循环的方法
res=[]
for x in 'spam':
 res.append(ord(x))

#map函数的方法
res=list(map(ord,'spam'))

#列表解析
res=[ord(x) for x in 'spam']

>>增加测试和嵌套循环
其实,列表解析还要比上面说的通用的多,我们可以在for之后编写一个if分支,用来增加选择逻辑。

#列表解析

[x ** 2 for x in range(10) if x % 2 == 0]


#map
list(map((lambda x:x**2),filter((lambda x: x % 2==0),range(10))))

上述的两行代码都是搜集了0~9中的偶数的平方和,可以很明显的看到,完成同样的功能,列表解析的语句简单地多。

实际上,列表解析还能够更加通用。你可以在一个列表解析中编写任意数量的嵌套的for循环,并且每一个都有可选的关联的if测试。通用结构如下所示:

expression for target1 in iterable1 [if comdition1]
      for target2 in iterable2 [if condition2] ...
      for targetN in iterableN [if conditionN]

当for分句嵌套在列表解析中时,它们工作起来就像等效的嵌套的for循环语句。例如,如下的代码:

res=[x+y for x in [0,1,2] for y in [100,200,300]]

与下面如此冗长的代码有相同的效果:

res=[]
for x in [0,1,2]:
 for y in [100,200,30]:
  res.append(x+y)

>>列表解析和矩阵
使用Python编写矩阵的一个基本的方法就是使用嵌套的列表结构,例如,下面的代码定义了两个3x3的矩阵:

M=[[1,2,3],
  [4,5,6],
  [7,8,9]]

N=[[2,2,2],
  [3,3,3],
  [4,4,4]]

列表解析也是处理这样结构的强大工具,它们能够自动扫描行和列。

取出第二列的所有元素:

[row[1] for row in M]   #[2,5,8]

[M[row][1] for row in (0,1,2)]  #[2,5,8]

取出对角线上的元素:

[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]]

 

 

上面的最后一个比较难于理解,它等同于如下基于语句的代码:

res=[]
for row in range(3):
 tmp=[]
 for col in range(3):
  tmp.append(M[row][col]*N[row][col])
 res.append(tmp)

>>理解列表解析
基于对运行在当前Python版本下的测试,map调用比等效的for循环要快两倍,而列表解析往往比map调用要稍快一些。速度上的差距来自底层实现,map和列表解析是在解释器中以C语言的速度来运行的,比Python的for循环在PVM中步进要快得多。

重访迭代器:生成器

如今的Python对延迟提供更多的支持——它提供了工具在需要的时候才产生结果,而不是立即产生结果。特别地,有两种语言结构尽可能地拖延结果创建。

生成器函数:编写为常规的def语句,但是是使用yield语句一次返回一个结果,在每个结果之间挂起和继续它们的状态。
生成器表达式:生成器表达式类似于上一小节的列表解析,但是,它们返回按需产生结果的一个对象,而不是构建一个结果列表。
由于上面二者都不会一次性构建一个列表,它们节省了内存空间,并且允许计算时间分散到各个结果请求。

>>生成器函数:yield VS return
之前我们写的函数都是接受输入参数并立即返回单个结果的常规函数,然而,也有肯能来编写可以送回一个值并随后从其退出的地方继续的函数。这样的函数叫做生成器函数,因为它们随着时间产生值的一个序列。

一般来说,生成器函数和常规函数一样,并且,实际上也是用常规的def语句编写的。然后,当创建时,它们自动实现迭代协议,以便可以出现在迭代背景中。

状态挂起

和返回一个值并退出的函数不同,生成器函数自动在生成值的时刻挂起并继续函数的执行。因此,它们对于提前计算整个一系列值以及在类中手动保存和恢复状态都很有用。由于生成器函数在挂起时保存的状态包含了它们的整个本地作用域,当函数恢复时,它们的本地变量保持了信息并且使其可用。

生成器函和常规函数之间的主要不同之处在于,生成器yield一个值,而不是return一个值。yield语句挂起该函数并向调用者发送一个值,但是,保留足够的状态以使得函数从它离开的地方继续。当继续时,函数在上一个yield返回立即继续执行。从函数角度来看,这允许其代码随着时间产生一系列的值,而不是一次计算它们并在诸如列表的内容中送回它们。

迭代协议整合

可迭代对象定义了一个__next__方法,它要么返回迭代中的下一项,要么引发一个特殊的StopIteration异常来终止迭代。一个对象的迭代器用iter内置函数接受。

如果支持该协议的话,Python的for循环以及其他的迭代技术,使用这种迭代协议来便利一个序列或值生成器;如果不支持,跌打返回去重复索引序列。

要支持这一协议,函数包含一条yield语句,该语句特别编译为生成器。当调用时,它们返回一个迭代器对象,该对象支持用一个名为__next__的自动创建的方法来继续执行的接口。生成器函数也可能有一条return语句,总是在def语句块的末尾,直接终止值的生成。

生成器函数应用

def gensquares(N):
 for i in range(N):
  yield i ** 2

这个函数在每次循环时都会产生一个值,之后将其返回给它的调用者。当它被暂定后,它的上一个状态保存了下来 并且在yield语句之后把控制器马上回收。它允许函数避免临时再做所有的工作,当结果的列表很大或者在处理每一个结果都需要很多时间时,这一点尤其重要。生成器将在loop迭代中处理一系列值的时间分布开来。

扩展生成器函数协议:send和next 在Python2.5中,生成器函数协议中增加了一个send方法。send方法生成一系列结果的下一个元素,这一点像__next__方法一样,但是它提供了一种调用者与生成器之间进行通信的方法,从而能够影响它的操作。

def gen():
 for i in range(10):
  X =yield i
  print(X)

G = gen()
next(G)     #0
G.send(77)    #77 1
G.send(88)    #88 2
next(G)     #None 3

上面的代码比较难于理解,而且书上的翻译比较劣质,没看懂。在网上查了一些资料,结合自己的理解,上述代码的运行过程应该是这样的:生成了一个函数对象,赋值给了G,然后调用了next() 函数,生成了生成器的第一个值0,所以返回值是0。此时函数运行到yield语句,碰到yield语句后立即挂起函数,保存状态,等待下一次迭代。程序中之后又调用了send()方法,将77传递给了yield语句,yield语句将send()传递过来的值(此处是77)赋值给X,然后打印出来。然后函数继续运行,直到再次碰到yield,此时是第二次碰到yield,所以返回了1,接着函数又被挂起,等待下一次迭代。接着又调用了send(),同上次调用一样,将传进的参数(此处是88)作为yield的返回值赋值给X,然后打印,接着继续运行函数,直到再次碰到yield,此时是第三次,因此输出2。最后又再次调用了next()函数,其实'next()'函数就是传递了一个None,因此,我们得到的结果是None和3。

此处需要注意的是,其实next()和send(None)是等价的。通过send()方法,我们就能够和生成器实现通信。

>>生成器表达式:迭代器遇到列表解析
在最新版的Python中,迭代器和列表解析的概念形成了这种语言的一个新特性——生成器表达式。从语法上讲,生成器表达式就像一般的列表解析一样,但是它们是扩在圆括号中而不是方括号中的。

[x ** 2 for x in range(4)]  #List comprehension:build a list
(x ** 2 for x in range(4))  #Genterator expression:make an iterable

从执行过程上来讲,生成器表达式很不相同:不是在内存中构建结果,而是返回一个生成器对象,这个对象将会支持迭代协议。

生成器表达式大体上可以认为是对内存空间的优化,他们不需要像方括号的列表解析一样,一次构造出整个结果列表。它们在实际中运行起来可能稍慢一些,所以它们可能只对于非常庞大的结果集合的运算来说是最优的选择。

>>生成器函数VS生成器表达式
生成器函数和生成器表达式自身都是迭代器,并由此只支持一次活跃迭代,我们无法有在结果集中位于不同位置的多个迭代器。

Python3.0解析语法概括

我们已经在本章中关注过列表解析和生成器,但是,别忘了,还有两种在Python3.0中可用的解析表达式形式:集合解析和字典解析。

[x*x for x in range(10)]   #List comprehension:build list
(x*x for x in range(10))   #Generator expression:produces items
{x*x for x in range(10)}   #Set comprehension:new in 3.0
{x:x*x for x in range(10)  #Directionary comprehension:new in 3.0

需要注意的是,上面最后两种表达方式都是一次性构建所有对象的,它们没有根据需要产生结果的概念。

总结

列表解析、集合解析、字典解析都是一次性构建对象的,直接返回的。
生成器函数和生成器表达式不是在内存中构建结果的,它们是返回一个生成器对象,这个对象支持迭代协议,根据调用者需要产生结果。
集合解析、字典解析都支持嵌套相关的if子句从结果中过滤元素。
函数陷阱

>>本地变量是静态检测的
Python定义的在一个函数中进行分配的变量名时默认为本地变量的,它们存在于函数的作用域并只在函数运行时存在。Python静态检测Python的本地变量,当编译def代码时,不是通过发现赋值语句在运行时进行检测的。被赋值的变量名在函数内部是当做本地变量来对待的,而不是仅仅在赋值以后的语句才被当做是本地变量。

>>没有return语句的函数
在Python函数中,return(以及yield)语句是可选的。从技术上说,所有的函数都会返回了一个值,如果没有提供return语句,函数将自动返回None对象。