文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Python 3 之 生成器详解

2023-01-31 07:52

关注

------ 生成器 ------------------------------------------------------------------

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

  • 生成器函数:编写为常规的def语句,但是使用yield语句一次返回一个结果,在每个结果之间挂起和继续它们的状态。

  • 生成器表达式:类似于列表解析,但是,它们返回按需产生结果的一个对象,而不是构建一个结果列表。


由于二者都不会一次性构建一个列表,它们节省了内存空间,并且允许计算时间分散到各个结果请求。我们将会看到,这二者最终都通过实现我们在前面介绍的迭代协议来执行它们延迟结果的魔术。


生成器函数: 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(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 0x0000014EF59FEDB0>

得到的是一个生成器对象,它支持迭代器协议,也就是所生成器对象有一个__next__方法,它可以开始这个函数,或者从它上次yield值后的地方恢复,并且在得到一系列的值的最后一个时,产生StopIteration异常。为了方便起见,next(x)内置函数为我们调用一个对象的X.__next__()方法:

>>> next(x)        # 类似py3 的 x.__next__()
0
>>> next(x)        # 在py2 中类似的方法为 x.next() 或 next()
1
>>> next(x)
4
>>> next(x)
9
>>> next(x)
Traceback (most recent call last):
  File "<pyshell#52>", line 1, in <module>
    next(x)
StopIteration

正如前面学过的,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 n: n ** 2), range(5)):
	print(x, end=" : ")
	
0 : 1 : 4 : 9 : 16 :

尽管如此,生成器在内存使用和性能方面都更好。它们允许函数避免临时再做所有的工作,当结果的列表很大或者在处理每一个结果都需要很多时间时,这一点尤其有用。生成器将在loop迭代中处理一系列值的时间分布开来。


尽管如此,对于更多高级的应用,它们提供了一个更简单的替代方案来手动将类的对象保存到迭代中的状态。 有了生成器,函数变量就能进行自动的保存和恢复。



扩展生成器函数协议:send 和 next

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


从技术上来说,yield现在是一个表达式的形式,可以返回传入的元素来发送,而不是一个语句[尽管无论哪种叫法都可以:作为yield X 或者 A = (yield X)]。表达式必须包括在括号中,除非它是赋值语句右边的唯一一项。 例如,X = yield Y没问题,就如同 X = (yield Y) + 42。


当使用这一额外的协议时,值可以通过调用G.send(value)发送给一个生成器G。之后恢复生成器的代码,并且生成器中的yield表达式返回了为了发送而传入的值。如果提前调用了正常的G.__next__()方法(或者其对等的next(G)),yield返回None。例如:

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

>>> G = gen()
>>> next(G)        # next() 开始生成器
0
>>> G.send(77)     # 高级的的send方法 发送参数给生成器表达式
77
1
>>> G.send(88)
88
2
>>> next(G)        # 返回None
None
3

例如,用send方法,编写一个能够被它的调用者终止的生成器。此外,在2.5版中,生成器还支持throw(type)的方法,它将生成器内部最后一个yield时产生一个异常以及一个close方法,它会在生成器内部产生一个终止迭代的新的GeneratorExit异常。这些都是我们这里不会深入学习的一些高级特性; 需要了解的请查看Python的标准库以获得更多的细节。


注意,尽管Python 3提供了一个next(X)方便的内置函数,它会调用一个对象的X.__next__方法,但是,其他的生成器方法,例如send,必须直接作为生成器对象的方法来调用(例如,G.send(X))。这么做是有意义的,你要知道,这些额外的方法只是在内置的生成器对象上实现,而__next__方法应用于所有的可迭代对象(包括内置类型和用户定义的类)。



生成器表达式:迭代器遇到列表解析

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

>>> [x ** 2 for x in range(4)]
[0, 1, 4, 9]

>>> (x ** 2 for x in range(4))        # 生成器表达式
<generator object <genexpr> at 0x0000014EF59FEDB0>

实际上,至少在一个函数的基础上,编写一个列表解析基本上等同于:在一个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#99>", 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调用中,还是需要额外的括号。

>>> 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'

>>> G = timesfour('spam')
>>> I = iter(G)
>>> next(I)
'ssss'
>>> next(I)
'pppp'

注意,我们使得这里的新的生成器再次迭代,正如下一小节所介绍的,生成器是单次迭代器。



生成器是单迭代器对象

生成器函数和生成器表达式自身都是迭代器,并由此只支持一次活跃迭代……不像一些内置类型,我们无法有在结果集中位于不同位置的多个迭代器。例如,使用前面小节的生成器表达式,一个生成器的迭代器是生成器之神(实际上,在一个生成器上调用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'
>>> next(I1)
'PPPP'
>>> I2 = iter(G)                        # ----
>>> next(I2)
'AAAA'

此外,一旦任何迭代器运行到完成,所偶的迭代器都将用尽,我们必须产生一个新的生成器以再次开始。

>>> list(I1)                            # 自动迭代
['MMMM']
>>> next(I2)                            # I2的手动迭代
Traceback (most recent call last):
  File "<pyshell#156>", line 1, in <module>
    next(I2)
StopIteration                           # 异常
>>> I3 = iter(G)                        # 生成新的迭代器(其实不会生成新的)
>>> next(I3)
Traceback (most recent call last):
  File "<pyshell#158>", line 1, in <module>
    next(I3)
StopIteration                            # 仍旧迭代异常
>>> I3 = iter(c * 4 for c in "SPAM")     # 新的迭代器
>>> next(I3)                             # 开始迭代
'SSSS'

对于生成器函数来说,也是如此,如下的基于语句的def等价形式只支持一个活跃的生成器并且在一次迭代之后用尽。

>>> def timesfour(S):
	for c in S:
		yield c * 4
		
>>> G = timesfour("spam")
>>> iter(G) is G
True
>>> I1, I2 = iter(G), iter(G)
>>> next(I1)
'ssss'
>>> next(I1)
'pppp'
>>> next(I2)
'aaaa'

这与某些内置类型的行为不同,它们支持多个迭代器并且在一个活动迭代器中传递并反映它们的原处修改。

>>> L = [1, 2, 3, 4]
>>> I1, I2 = iter(L), iter(L)
>>> next(I1)
1
>>> next(I1)
2
>>> next(I2)
1
>>> del L[2:]
>>> next(I1)
Traceback (most recent call last):
  File "<pyshell#180>", line 1, in <module>
    next(I1)
StopIteration

当我们开始别写基于类的迭代器时,我们将看到,由我们来决定想要为自己的对象支持多个迭代器。



用迭代工具模拟zip 和 map

要说明应用迭代工具的能力,让我们来看一些高级用例。一旦你了解了列表解析、生成器 和 其他的迭代工具,就知道模拟众多的Python的函数式内置工具既直接又很有益。


例如,我们已经看到了内置的zip和map函数如何组合可迭代对象和映射函数。使用多个序列参数,map以与zip配对元素相同的方式,把函数映射到取自每个序列的元素。

>>> S1 = "abc"
>>> S2 = "xyz123"
>>> list(zip(S1, S2))
[('a', 'x'), ('b', 'y'), ('c', 'z')]

>>> list(zip([-2, -1, 0, 1, 2]))
[(-2,), (-1,), (0,), (1,), (2,)]
 
>>> list(zip([1, 2, 3], [2, 3, 4, 5]))
[(1, 2), (2, 3), (3, 4)]
 
>>> list(map(abs, [-2, -1, 0, 1, 2]))
[2, 1, 0, 1, 2]

>>> list(map(pow, [1, 2, 3], [2, 3, 4, 5]))
[1, 8, 81]

尽管它们用于不同的目的,如果你研究这些示例足够长的时间,可能会注意到zip结果和执行map的函数参数之间的一种关系,下面的例子说明这种关系。



编写自己的map(func, ...)

尽管map和zip内置函数快速而方便,总是可以在自己的代码中模拟它们。例如,在之前,我们看到了一个函数针对单个的序列参数来模拟map内置函数。针对多个序列的时候也并不会费太多工夫就可以像内置函数那样操作。

>>> def mymap(func, *seqs):
	res = []
	for args in zip(*seqs):
		res.append(func(*args))
	return res

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

这个版本很大程度上依赖于特殊的*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调用迫使生成器运行,通过激活迭代协议而进行。生成器由这些函数自身返回,也由它们说使用的Python 3.x式的zip内置函数返回,根据需要产生结果。



编写自己的zip(...)和map(None, ...)

当然,目前给出的示例中的很多魔力在于,它们使用zip内置函数来配对来自多个序列的参数。我们也注意到,我们的map近似版确实是模拟了Python 3.x的map的行为,它们从最短的序列的长度处截断,并且,当长度不同的时候,它们不支持补充结果的思路,就像Python 2.x中带有一个None参数的map所做的一样:

>>> map(None, [1, 2, 3], [2, 3, 4, 5])            # Python 2.6
[(1, 2), (2, 3), (3, 4), (None, 5)]
>>> map(None, 'abc', 'xyz123')                    # Python 2.6
[('a', 'x'), ('b', 'y'), ('c', 'z'), (None, '1'), (None, '2'), (None, '3')]

使用迭代工具,我们可以编写近似版来模拟断的zip和Python 2.6的补充的map,这些其实在代码上近乎是相同的:

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

>>> def mymapPad(*seqs, pad=None):
	seqs = [list(S) for S in seqs]
	res = []
	while any(seqs):
		res.append(tuple((S.pop(0) if S else pad) for S in seqs))
	return res

>>> S1, S2 = 'abc', 'xyz123'
>>> print(myzip(S1, S2))
[('a', 'x'), ('b', 'y'), ('c', 'z')]
>>> print(mymapPad(S1, S2))
[('a', 'x'), ('b', 'y'), ('c', 'z'), (None, '1'), (None, '2'), (None, '3')]
>>> print(mymapPad(S1, S2, pad=99))
[('a', 'x'), ('b', 'y'), ('c', 'z'), (99, '1'), (99, '2'), (99, '3')]

这里编写的函数可以在任何类型的可迭代对象上运行,因为它们通过list内置函数来运行自己的参数以迫使结果生成(例如,文件像参数一样工作,此外,序列像字符串一样)。注意这里的all和any内置函数的使用,如果一个可迭代对象中的所有或任何元素为True(或者对等的为非空),它们分别返回True。当列表中的任何或所有参数在删除后变成了空,这些内置函数将用来停止循环。


还要注意Python 3.x的keyword-only参数pad,和Python 2.6的map不同,我们的版本将允许指定任何补充的对象(如果你使用Python 2.6,使用一个**kargs形式来支持这一选项)。当这些函数运行的时候,打印出如下的结果……一个zip和两个补充的map。

[('a', 'x'), ('b', 'y'), ('c', 'z')]
[('a', 'x'), ('b', 'y'), ('c', 'z'), (None, '1'), (None, '2'), (None, '3')]
[('a', 'x'), ('b', 'y'), ('c', 'z'), (99, '1'), (99, '2'), (99, '3')]

这些函数不能够用于列表解析转换,因为它们的循环太具体了。然而,和前面一样,既然我们的zip和map近似版构建并返回列表,用yield将它们转换为生成器以便它们每个都是每次返回结果中的一项,这还是很容易做到的。结果和前面的相同,但是,我们需要再次使用list来迫使该生成器产生其值以供显示。

>>> def myzip(*seqs):
	seqs = [list(S) for S in seqs]
	res = []
	while all(seqs):
		yield tuple(S.pop(0) for S in seqs)
		
>>> def mymapPad(*seqs, pad=None):
	seqs = [list(S) for S in seqs]
	res = []
	while any(seqs):
		yield tuple((S.pop(0) if S else pad) for S in seqs)
		
>>> S1, S2 = 'abc', 'xyz123'
>>> print(myzip(S1, S2))
<generator object myzip at 0x00000161CDB15FC0>
>>> print(mymapPad(S1, S2))
<generator object mymapPad at 0x00000161CDB15F68>
>>> print(mymapPad(S1, S2, pad=99))
<generator object mymapPad at 0x00000161CDB15FC0>

最后,这里是我们的zip和map模拟器的替代实现……下面的版本不是使用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)]

>>> def mymapPad(*seqs, pad=None):
	maxlen = max(len(S) for S in seqs)
	index = range(maxlen)
	return [tuple((S[i] if len(S) > i else pad) for S in seqs) for i in index]

>>> S1, S2  = 'abc', 'xyz123'
>>> print(myzip(S1, S2))
[('a', 'x'), ('b', 'y'), ('c', 'z')]
>>> print(mymapPad(S1, S2))
[('a', 'x'), ('b', 'y'), ('c', 'z'), (None, '1'), (None, '2'), (None, '3')]
>>> print(mymapPad(S1, S2, pad=99))
[('a', 'x'), ('b', 'y'), ('c', 'z'), (99, '1'), (99, '2'), (99, '3')]

由于这些代码使用len和索引,它们假设参数是序列或类似的,而不是任意的可迭代对象。这里,外围的解析遍历参数索引范围,内部的解析(传递到元组)遍历传入的序列以并列地提取参数。当它们运行时,结果和前面相同。


更有趣的是,生成器和迭代器似乎在这个例子中泛滥。传递给min 和 max的参数是生成器表达式,它在嵌套和解析开始迭代之前运行完成。此外,嵌套的列表解析使用了两个层级的延迟运算……Python 3.x的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)如何只支持一个单个的遍历,并且在发生之后为空,我提过会给出一个示例展示这在实际中是如何变得微妙而重要的。现在,已经学习了关于迭代话题的许多内容,我可以兑现这个承诺了。考虑到本篇的zip模拟示例的更优替代代码,该示例从Python手册中高端一个例子改编而来。

>>> def myzip(*args):
	iters = map(iter, args)
	while iters:
		res = [next(i) for i in iters]
		yield tuple(res)

由于这段代码使用iter和next,它对任何类型的可迭代对象都有效。注意,当这个参数的迭代器之一用尽时,没有任何理由捕获由这个解析内的next(it)来引发的StopIteration——允许它传递会终止这个生成器函数,并且与一条return语句具有相同的效果。如果至少传递了一个参数的话,while iters:对于循环来说足够了,并且,避免了无限循环(列表解析将总是返回一个空的列表):


这段代码在Python 2.6中也工作的很好,如下所示:

>>> list(myzip('abc', 'lmnop'))
[('a', 'l'), ('b', 'm'), ('c', 'n')]

但是,在Python 3.x中,它陷入了一个无限循环中并失效。

>>> list(myzip('abc', 'lmnop'))

wKiom1bP4WeD2qV1AAB5YixRPvI006.png

因为Python 3.x的map返回一个单次可迭代对象,而不是像Python 2.6中那样的一个列表。在Python 3.x中,只要我们在循环中运行了一次列表解析,iters将会永远为空(并且res将会是[])。为了使其在Python 3.x下正常工作,我们需要使用list内置函数来创建一个支持多次迭代的对象:

>>> def myzip(*args):
	iters = list(map(iter, args))
	while iters:
		res = [next(i) for i in iters]
		yield tuple(res)
		
>>> list(myzip('abc', 'lmnop'))
[('a', 'l'), ('b', 'm'), ('c', 'n')]

自己运行并跟踪其操作。这里要记住的是:在Python3.x中把map调用放入到list调用中不仅是为了显示。



内置类型和类中的值生成

最后,尽管我们在本篇中关注自己编写值生成器,别忘了,很多内置的类型以类似的方式工作……正如我们在之前看到的一样,例如,字典拥有在每次迭代中产生键的迭代器。

>>> D = {'a':1, 'b':2, 'c':3}
>>> x = iter(D)
>>> next(x)
'a'
>>> next(x)            # 这里并不一定就是创建字典时候的第二个键,因为字典是无序的。
'b'

和手动编写的生成器所产生的值一样,字典键也可以手动迭代,或者使用包括for循环、map调用、列表解析和我们在前面介绍的很多其他环境等在内的自动迭代工具。

>>> for key in D:
	print(key, D[key])
	
a 1
b 2
c 3

正如我们所看到的,在文件迭代器中,Python简单地载入了一个文件的行。

>>> for line in open('NEWS.txt'):
	print(line, end='')

	
+++++++++++
Python News
+++++++++++

What's New in Python 3.5.1 final?
=================================
.....

尽管内置类型迭代器绑定到了一个特定类型的值生成,概念与我们使用表达式和函数编写的生成器是类似的。像for循环这样的迭代环境接受任何的可迭代对象,不管是用户定义的还是内置的。


尽管这超出了本篇的讨论范围,还是可能用遵守迭代协议的类来实现任意的用户定义的生成器对象。这样的类定义了一个特别的__iter__方法,它由内置的iter函数调用,将返回一个对象,该对象有一个__next__方法,该方法由next内置函数调用(一个__getitem__索引方法作为迭代的退而求其次的选项也是可以的)。


从这样的一个类创建的实例对象,看做是可迭代的,并且可以用在for循环和所有其他的迭代环境中。然而,有了类,我们可以访问比其他生成器构造所能提供的更丰富的逻辑和数据结构选型。迭代器的内容不会真正结束,直到我们了解到它如何映射到类。


.

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯