返回介绍

重访迭代器:生成器

发布于 2024-01-29 22:24:16 字数 4357 浏览 0 评论 0 收藏 0

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

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

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

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

生成器函数:yield VS return

在本书的这一部分中,我们已经学习了编写接收输入参数并立即送回单个结果的常规函数。然而,也有可能来编写可以送回一个值并随后从其退出的地方继续的函数。这样的函数叫做生成器函数,因为它们随着时间产生值的一个序列。

一般来说,生成器函数和常规函数一样,并且,实际上也是用常规的def语句编写的。然而,当创建时,它们自动实现迭代协议,以便可以出现在迭代背景中。我们在第14章学习了迭代器,这里,我们将再次回顾它们,看看它们是如何与生成器相关的。

状态挂起

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

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

迭代协议整合

要真正地理解生成器函数,我们需要知道,它们与Python中的迭代协议的概念密切相关。正如我们已经看到的,可迭代的对象定义了一个__next__方法,它要么返回迭代中的下一项,或者引发一个特殊的StopIteration异常来终止迭代。一个对象的迭代器用iter内置函数接收。

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

要支持这一协议,函数包含一条yield语句,该语句特别编译为生成器。当调用时,它们返回一个迭代器对象,该对象支持用一个名为__next__的自动创建的方法来继续执行的接口。生成器函数也可能有一条return语句,总是在def语句块的末尾,直接终止值的生成。从技术上讲,可以在任何常规函数退出执行之后,引发一个StopIteration异常来现实。从调用者的角度来看,生成器的__next__方法继续函数并且运行到下一个yield结果返回或引发一个StopIteration异常。

直接效果就是生成器函数,编写为包含yield语句的def语句,自动地支持迭代协议,并且由此可能用在任何迭代环境中以随着时间并根据需要产生结果。

注意:正如第14章所提到的,在Python 2.6和更早的版本中,可迭代的对象定义了一个名为next的方法而不是__next__方法。这包括我们在这里使用的生成器对象。在Python 3.0中,这个方法重命名为__next__。next内置函数作为一个方便的、可移植的工具提供:next(I)等同于Python 3.0中的I.__next__()和Python 2.6中的I.next()。在Python 2.6之前,程序直接调用I.next()而不是手动地迭代。

生成器函数应用

为了讲清楚基础知识,请看如下代码,它定义了一个生成器函数,这个函数将会用来不断地生成一系列的数字的平方。

这个函数在每次循环时都会产生一个值,之后将其返还给它的调用者。当它被暂停后,它的上一个状态保存了下来,并且在yield语句之后控制器马上被回收。例如,当用在一个for循环中时,在循环中每一次完成函数的yield语句后,控制权都会返还给函数。

为了终止生成值,函数可以使用一个无值的返回语句,或者在函数主体最后简单地让控制器脱离。

如果想要看看在for里面发生了什么,直接调用一个生成器函数:

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

正如我们在第14章所学习过的,for循环(以及其他的迭代环境)以同样的方式与生成器一起工作:通过重复调用__next__方法,直到捕获一个异常。如果一个不支持这种协议的对象进行这样迭代,for循环会使用索引协议进行迭代。

注意在这个例子中,我们能够简单地一次就构建一个所获得的值的列表。

对于这样的例子,我们还能够使用for循环、map或者列表解析的技术来实现:

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

尽管如此,对于更多高级的应用,它们提供了一个更简单的替代方案来手动将类的对象保存到迭代中的状态(更多关于类的内容在稍后的第六部分介绍)。有了生成器,函数变量就能进行自动的保存和恢复[1]

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

在Python 2.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。例如:

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

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

[1]有趣的是,生成器函数也是某些“穷人的”多线程设备。它们通过将操作划分为在yield之间运行的步骤,从而在函数的工作之间插入其调用者的工作。然而,生成器不是线程,程序在一个单线程控制中,显式地导入或导出函数。从某种意义上讲,线程更为通用(生成者可以真正独立地运行,并且把结果发布到一个队列),但是,生成器可能更容易编写。参见本书第17章中的第2个脚注对Python多线程工具的简单介绍。注意,由于控制在yield和下一个调用处显式地导向,生成器也是不能回溯的,但是,它与coroutine关系更密切,后者是超出本书讨论范围的正式概念。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文