Python: generator, yield, yield from 详解

1.Generator Expressions

生成器表达式是用小括号表示的简单生成器标记法:

generator_expression ::= "(" expression comp_for ")"

生成器表达式产生一个生成器对象,它的语法和for类似,出了它是被“()”包含,而不是[]或{};

生成器表达式中变量的计算被延迟到__next__()函数的调用,然而最左边for循环子句被立即计算,这样,如果他有错误的话可以被立即看到。后面的for循环子句不能被立即计算,因为他们可能依赖于前面的for循环,例如(x*y for x in range(10) for y in bar(x))

python3.6以后,如果生成器出现在async def function中,那么async for子句和await表达式可以被理解为是异步的。如果生成器表达式包含async for子句或者await表达式,就叫做异步生成器表达式。异步生成器表达式产生一个新的异步生成器对象,它是一个异步迭代器。

2. Yield Expressions

yield_atom       ::=  "(" yield_expression ")"
yield_expression ::=  "yield" [expression_list | "from" expression]
Yield表达式用于定义一个生成器函数或异步生成器函数,因此只能被用于一个函数定义体内。在一个函数定义体中使用yield表达式使其成为生成器,
在一个async def函数体内使用yield表达式使协程函数成为一个异步生成器。例如:
def gen(): //定义一个生成器函数
yield 123
async def agen(): //定义一个异步生成器函数
yield 123
当一个生成器函数被调用,它返回一个迭代器,也叫生成器。这个生成器控制生成器函数的执行。当生成器的一个函数被调用的时候,生成器函数开始执行。
执行到第一个yield表达式时,挂起,返回expression_list的值给调用者。对于挂起,我们指的是所有局部状态被保留,包含当前局部变量的绑定,
指令指针,内部调用栈,任何异常处理状态。当通过调用生成器的一个函数恢复执行时,函数的执行就好像是从外部再次调用yield表达式一样。恢复执行后,
yield表达式的值取决调用的方法。如果是__next__()被调用(一般通过for循环或者内置的next()函数),那么值是None。如果是send()被调用,
值是传给send的参数的值。
所有的这些使得生成器函数非常像协程;它产生多次值,有多个入口点并且执行可以被挂起。唯一的不同是,生成器函数yield后不能控制程序从哪里继续执行,
控制权总是传回给生成器的调用者。
yield表达式可以在try块的任何地方,如果生成器在被结束(到达零引用或者因为垃圾回收机制)之前没有被恢复执行,生成器的close函数被调用,因此finally
子句被执行。
当yield from <expr> 被使用,它把附加的表达式当成一个子迭代器,所有子迭代器产生的值被直接传回给当前生成器函数的调用者。当前生成器调用send的参数值
和调用throw的异常参数 都将被传给底层迭代器(子迭代器),如果他有对应的方法的话。否则,send导致AttributeError或者TypeError,而throw立即
raise传给他的异常。
当底层迭代器执行完成,StopIteration对象的value值变成这个yield from表达式的值。这个值可以被显式的设置当产生StopIteration时,或者自动设置,如果
子迭代器是一个生成器(子生成器返回一个值)
3. 生成器-迭代器 方法
这部分介绍生成器迭代器的方法,他们可以被用来控制生成器函数的执行。当生成器正在执行时调用这些函数将导致ValueError。
(1)generator.__next__()
开始生成器函数的执行或者从最后被执行的yield表达式中恢复执行,如果是恢复执行,yield表达式的值是None,继续执行到下一个yield表达式,
挂起,返回expression_list的值给__next__()的调用者。如果生成器没有再yield一个值,则产生一个StopIteration异常
这个方法一般被隐式调用,例如for循环,next()
(2)generator.send(value)
恢复执行并且发送一个值到生成器函数。这个value参数就是当前yield表达式的结果。send方法返回下一个生成器yield的值
如果没有yield,返回StopIteration。当用send来启动生成器,他的参数必须是None,因为没有yield表达式接收值。
(3)generator.throw(type[,value[,traceback]])
在生成器挂起的地方产生一个type类型的异常,并且返回下一个生成器函数yield的值,如果没有yield,返回StopIteration。
如果生成器函数没有捕获这个传进去的异常 ,或者产生了另一个不同的异常,那么将这个异常传递给调用者。
(4)generator.close()
在生成器挂起的地方产生一个GeneratorExit(),如果生成器之后优雅的退出,已经关闭,或者产生了一个GeneartorExit(不捕获该异常),close将返回到它的
调用者。如果生成器yield一个值,那么产生一个RuntimeError。如果生成器产生了任何其他异常,将传递给调用者。如果
生成器因为一个异常或者正常退出,那么close不做任何事情。
实例:
>>> def echo(value=None):
...     print("Execution starts when 'next()' is called for the first time.")
...     try:
...         while True:
...             try:
...                 value = (yield value)
...             except Exception as e:
...                 value = e
...     finally:
...         print("Don't forget to clean up when 'close()' is called.")
...
>>> generator = echo(1)
>>> print(next(generator))
Execution starts when 'next()' is called for the first time.
1
>>> print(next(generator))
None
>>> print(generator.send(2))
2
>>> generator.throw(TypeError, "spam")
TypeError('spam',)
>>> generator.close()
Don't forget to clean up when 'close()' is called.
PEP380 加入了yield from表达式,允许一个生成器委派部分操作给另一个生成器。这可以剔除一部分包含yield的代码放到另一个生成器。另外,
子生成器可以返回一个值,这个值对于委托生成器也是可用的。
虽然主要涉及用来委派一个子生成器,但是yield from 表达式事实上可以委派任何的子迭代器。
低于简单的迭代器, yield from iterable 本质上就是一个简短的形式:for item in iterable: yield item,例如:
>>> def g(x):
...     yield from range(x, 0, -1)
...     yield from range(x)
...
>>> list(g(5))
[5, 4, 3, 2, 1, 0, 1, 2, 3, 4]
然而,不像普通的循环,yield from 允许子生成器直接从调用区域接收send和throw的值,并且返回一个最后的值给外层生成器。示例如下:
>>> def accumulate():
...     tally = 0
...     while 1:
...         next = yield
...         if next is None:
...             return tally
...         tally += next
...
>>> def gather_tallies(tallies):
...     while 1:
...         tally = yield from accumulate()
...         tallies.append(tally)
...
>>> tallies = []
>>> acc = gather_tallies(tallies)
>>> next(acc)  # Ensure the accumulator is ready to accept values
>>> for i in range(4):
...     acc.send(i)
...
>>> acc.send(None)  # Finish the first tally
>>> for i in range(5):
...     acc.send(i)
...
>>> acc.send(None)  # Finish the second tally
>>> tallies
[6, 10]