一、生成器
Python 提供更多的对延迟的支持——它提供了工具在需要的时候才产生结果,而不是立即产生结果。有两种语言结构尽可能的延迟结果创建。
生成器函数: 编写为常规的 def 语句,但是使用 yield 语句一次返回一个结果,在每个结果之间挂起和继续它们的状态。
生成器表达式: 类似于列表解析,但是,它们返回一个按需产生结果的对象,而不是构建一个结果列表。
由于二者都不会一次性的构建一个列表,它们节省了内存空间,并且允许计算时间分散到各个结果请求。
1、生成器函数:yield
我们可以编写返回一个值并随后从其退出的地方继续执行的函数。这样的函数叫做生成器函数。因为它们随着时间产生值的一个序列。
一般来说,生成器函数和常规函数一样,并且,实际上也是用常规的 def 语句编写的。只是不是使用 return 语句返回一个值,而是使用 yield 语句,当创建时,它们自动实现迭代协议,以便在迭代环境中被迭代。
这个函数在每次循环时都会产生一个值,将其返还给它的调用者。当它被暂停后,它的上一个状态保存了下来,并且在 yield 语句之后控制器马上被回收。直接调用生成器函数会产生一个生成器对象,它支持迭代器协议,也就是说,生成器对象有一个 __next__ 方法,它可以开始这个函数,或者从它上次 yield 值后的地方恢复,并且在得到一系列的值的最后一个时,产生 StopIteration 异常。
next() 内置函数会为我们调用一个对象的 __next__() 方法。
相较于传统函数,只能一次返回所需的所有值,生成器在内存使用和性能方面都更好,他们允许函数避免临时再做所有的工作,当结果的列表很大或者在处理每一个结果都需要很多时间时,这一点尤其有用。生成器将在 loop 迭代中处理一系列值的时间分布开来。有了生成器,函数变量就能进行自动的保存和恢复。
状态挂起
和返回一个值并退出的常规函数不同,生成器函数自动在生成值的时刻挂起并等待继续函数的执行。因此,它们对于计算一系列值以及在类中手动保存和恢复状态都很有用。由于生成器函数在挂起时保存的状态包含它们的整个本地作用域,当函数恢复时,它们的本地变量保持了信息并且可用。
生成器函数和常规函数之间的主要的代码不同之处在于,生成器 yield 返回一个值,而不是 return 返回一个值。yield 语句挂起该函数并向调用者发送回一个值,但是,保留足够的状态以使得函数能够从它离开的地方继续。当继续时,函数在上一个 yield 返回后立即继续执行。从函数的角度来看,这允许其代码随着时间产生一系列的值,而不是一次计算它们并在诸如列表的内容中返回它们。
迭代协议
生成器函数与 Python 中的迭代协议的概念密切相关。可迭代对象的迭代器(自己的或者靠 iter() 生成的)定义了一个__next__() 方法,它要么返回迭代中的下一项,或者引发一个特殊的 StopIteration 异常来终止迭代。
要支持这一协议,函数需要包含一条 yield 语句,该语句特别编译为生成器。当调用函数时,它们返回一个可迭代对象,该对象拥有自己自动创建的 __next__() 方法。生成器函数也可能有一条 return 语句,总是在 def 语句块的末尾,直接终止值的生成。从技术上讲,可以在任何常规函数退出执行之后,引发一个 StopIteration 异常来实现。从调用者的角度来看,生成器的 __next__() 方法继续函数并且运行到下一个 yield 结果返回或引发一个 StopItertio 异常。
直接效果就是生成器函数,编写为包含 yield 语句的 def 语句,自动地支持迭代协议,并且可用在任何迭代环境中以随着时间并根据需要产生结果。
2、扩展生成器函数协议:send() 和 next()
send() 方法生成一系列结果的下一个元素,这一点就像 __next__() 方法一样,但是它提供了一种调用者与生成器之间进行通信的方法,从而能够影响它的操作。
生成器可以通过调用 G.send(value) 方法将参数 value 发送给生成器 G。之后恢复生成器的代码,并且生成器中的 yield 表达式返回了发送过来的值。如果调用了正常的G.__next__() 方法,yield 返回 None。
从技术上讲,yield 现在是一个表达式的形式,可以返回传入的元素,而不是一个语句。表达式必须包含在括号中,除非它是赋值语句右边的唯一一项。
next() 方法为内置函数,但是生成器方法 send() 必须作为生成器对象的方法来调用。因为这些生成器方法只是在内置的生成器对象上实现,而 __next__() 方法应用于所有的可迭代对象(包括内置类型和与用户自定义类型)。
3、生成器表达式
从语法上来讲,生成器表达式就像一般的列表解析一样,但是它们是扩在圆括号中而不是方括号中的。 编写一个列表基本上等同于:在一个 list() 内置调用中包含一个生成器表达式以迫使其一次生成列表中所有的结果。 生成器表达式不是在内存中构建结果,而是返回一个生成器对象,这个对象将会支持迭代协议并在任意的迭代语境的操作中。 我们一般不会机械的使用 next() 迭代器来操作生成器表达式,因为 for 循环会自动触发。如果生成器表达式大体上可以认为是对内存空间的优化,它们不需要像方括号的列表解析一样,一次构造出整个结果列表。它们在实际中运行起来可能稍慢一些,但是它们对于非常大的结果集合的运算来说是最优的选择。
4、生成器函数 VS 生成器表达式
同样的迭代往往可以用一个生成器函数或一个生成器表达式编写。等价的生成器函数需要略微多一些的代码,但是,作为一个多语句的函数,如果需要的话,它能够编写更多的逻辑并使用更的状态信息。
5、生成器是单迭代对象
生成器函数和生成器表达式自身就是迭代器,并由此只支持一次活跃迭代。因为生成器的迭代器是生成器自身,所以在一个生成器上调用 iter() 没有任何效果。 如果你手动的使用多个迭代器来迭代结果流,他们将会指向相同的位置。 一旦任何迭代器运行到完成,所有的迭代器都将用尽,我们必须产生一个新的生成器以再次开始迭代。 这与某些内置类型的行为不同,它们支持创建多个迭代器并且在一个活动迭代器中传递并返映它们的原处修改。《Python基础手册》系列: