python yield 和 yield from
python中yield item 会产出一个值,提供诶next(...)的调用方;此外,还会做出让步 暂停执行生成器,让调用方继续工作,直到下次调用next()。以上语法看出,协程和生成器类似,都是定义体中包含yield关键字的函数。可是协程中,yield通常出现在表达式右侧(datum = yield),yield关键字后买呢没有表达式。协程可能会从调用方接收数据,调用方使用.send(datum)吧数据提供给协程。
一:生成器如何进化成协程
自python中加入yield关键字后,又经过了一系列的演化:yield 关键字可以在表达式中使用(a = yield b);生成器API中增加了.send(value)方法(生成器的调用方可以使用.send(...)方法发送数据,发送的数据会成为生成器函数中yield 表达式的值);PEP 342添加了.throw(...)和.close()方法(前者的作用是让调用方抛出异常,在生成器中处理;后者的作用是终止生成器);因此,生成器可以作为协程使用。协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值。
协程最近的演进来自Python3.3实现的“PEP380—Syntaxfor Delegating to a Subgenerator”(https://www.python.org/dev/peps/pep-0380/)。PEP380对生成器函数的句法做了两处改动:生成器可以返回一个值;以前如果在生成器中给return 语句提供值,会抛出SyntaxError异常;新引入了yield from 句法,使用它可以把复杂的生成器重构成小型的嵌套生成器,省去了之前把生成器的工作委托给子生成器所需的大量样板代码。
def foo(num): print('starting...') while num < 100: num += 1 yield num g = foo(0) for i in g: print(i)
二:用作协程的生成器的基本行为
协程可以身处四个状态中的一个。当前状态可以使用inspect.getgeneratorstate(...)
函数确定,该函数会返回下述字符串中的一个。
GEN_CREATED:等待开始执行;
GEN_RUNNING:解释器正在执行(只有在多线程应用中才能看到这个状态);
GEN_SUSPENDED:在
yield 表达式处暂停;
GEN_CLOSED:执行结束
创建协程的方式与创建生成器一样,通过调用函数的方法获取到一个生成器对象。紧接着调用next()方法来启动生成器,这一步也称为prime,有些文章会把这个东西翻译成 "预激",即让协程开始执行到第一个yield表达式的位置。(为了方便下文都称为预激了),预激协程的装饰器如果不预激,那么协程没什么用。调用 g.send(x) 之前,记住一定要调用next(g)。为了简化协程的用法,有时会使用一个预激装饰器。
def coroutine(func): def primer(*args, **kwargs): gen = func(*args, **kwargs) next(gen) return gen return primer @coroutinedef foo(): sum = 0 count = 0 avg = 0 while True: num = yield avg sum += num #10 count += 1 #1 avg = sum / count g = foo() print(next(g)) print("*" * 20) print(g.send(5)) print(g.send(6))
这个无限循环表明,只要调用方不断把值发给这个协程,它就会一直接收值,然后生成结果。仅当调用方在协程上调用 .close() 方法,或者没有对协程的引用而被垃圾回收程序回收时,这个协程才会终止。
*注意,使用 yield from 句法调用协程时,会自动预激。
print((g.send('haha'))) print(g.send(1)) Traceback (most recent call last): File "/Users/lianghui/mycodes/python_codes/python3/yield&yield_from.py", line 109, in <module> print((g.send('haha'))) File "/Users/lianghui/mycodes/python_codes/python3/yield&yield_from.py", line 95, in foo sum += num #10TypeError: unsupported operand type(s) for +=: 'int' and 'str'Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
由于协程内没有处理异常,协程会终止。如果试图重新激活协程,会抛出StopIteration异常
python2.5开始,客户代码可以在生成器对象上调用throw和close,显式的吧异常发给协程
1、generator.throw(exc_type[, exc_value[, traceback]])
使生成器在暂停的yield表达式处抛出指定异常。如果生成器处理了抛出的异常,代码会向前执行到写一个yield表达式,而产出的值会成为调用 generator.throw方法得到的返回值。如果生成器没有处理抛出的异常,异常会向上冒泡,传到调用方的上下文中。
2:generator.close()
使生成器在暂停的 yield 表达式处抛出 GeneratorExit 异常。如果生成器没有处理这个异常,或者抛出了 StopIteration 异常(通常是指运行到结尾),调用方不会报错。如果收到 GeneratorExit 异常,生成器一定不能产出值,否则解释器会抛出RuntimeError 异常。生成器抛出的其他异常会向上冒泡,传给调用方。
from inspect import getgeneratorstateclass TestException(Exception): """test exception""" def demo_exception_handling(): print('starting...') while True: try: x = yield except TestException: print('**** TestException handled Continue..') else: print('coroutine recevied: {!r}'.format(x)) raise RuntimeError('this line should never run.') exc_coro = demo_exception_handling() print(next(exc_coro)) print(exc_coro.send(11)) print(exc_coro.send(12)) exc_coro.throw(TestException) print(getgeneratorstate(exc_coro)) exc_coro.close() print(getgeneratorstate(exc_coro))
让协程返回值
在Python2中,生成器函数中的return不允许返回附带返回值。在Python3中取消了这一限制,因而允许协程可以返回值
from collections import namedtuple Result = namedtuple('Result', 'count average') def averager(): total = 0.0 count = 0 average = None while True: term = yield average if term is None: break total += term count += 1 average = total / count return Result(count, average) coro_avg = averager() print(next(coro_avg)) print(coro_avg.send(10)) print(coro_avg.send(30)) print(coro_avg.send(6.5)) print(coro_avg.send(None))
发送None会终止循环,导致协程结束,返回结果。一如既往,生成器对象会抛出StopIteration异常。异常对象的value属性保存着返回的值。
注意,return 表达式的值会偷偷传给调用方,赋值给StopIteration异常的一个属性。这样做有点不合常理,但是能保留生成器对象的常规行为——耗尽时抛出StopIteration 异常。如果需要接收返回值,可以这样:
try: coro_avg.send(None) except StopIteration as exc: result = exc.value print(result)
获取协程的返回值要绕个圈子,可以使用Python3.3引入的yield from获取返回值。yield from 结构会在内部自动捕获 StopIteration 异常。这种处理方式与 for 循环处理 StopIteration 异常的方式一样。对 yield from 结构来说,解释器不仅会捕获 StopIteration 异常,还会把value 属性的值变成 yield from 表达式的值。
使用yield from
yield from 是 Python3.3 后新加的语言结构。在其他语言中,类似的结构使用 await 关键字,这个名称好多了,因为它传达了至关重要的一点:在生成器 gen 中使用 yield from subgen() 时,subgen 会获得控制权,把产出的值传给 gen 的调用方,即调用方可以直接控制 subgen。与此同时,gen 会阻塞,等待 subgen 终止。
yield from 可用于简化 for 循环中的 yield 表达式。例如:
def gen(): for c in 'AB': yield c for i in range(1, 3): yield i print(list(gen()))#可以用yield from改为def gen1(): yield from 'AB' yield from range(1, 3) print(list(gen1()))
yield from x 表达式对 x 对象所做的第一件事是,调用 iter(x),从中获取迭代器。因此,x 可以是任何可迭代的对象。
yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,
****而不用在位于中间的协程中添加大量处理异常的样板代码。有了这个结构,协程可以通过以前不可能的方式委托职责。*******
# PEP 380 使用了一些yield from使用的专门术语:
# 委派生成器:包含 yield from <iterable> 表达式的生成器函数;
# 子生成器:从 yield from 表达式中 <iterable> 部分获取的生成器;
# 调用方:调用委派生成器的客户端代码;
# 下图是这三者之间的交互关系:
委派生成器在 yield from 表达式处暂停时,调用方可以直接把数据发给子生成器,子生成器再把产出的值发给调用方。子生成器返回之后,解释器会抛出StopIteration 异常,并把返回值附加到异常对象上,此时委派生成器会恢复。
下面是一个求平均身高和体重的示例代码:
from collections import namedtuple Result = namedtuple('Result', 'count average') # 子生成器 def averager(): total = 0.0 count = 0 average = None while True: # main 函数发送数据到这里 print("in averager, before yield") term = yield if term is None: # 终止条件 break total += term count += 1 average = total / count print("in averager, return result") return Result(count, average) # 返回的Result 会成为grouper函数中yield from表达式的值 # 委派生成器 def grouper(results, key): # 这个循环每次都会新建一个averager 实例,每个实例都是作为协程使用的生成器对象 while True: print("in grouper, before yield from averager, key is ", key) results[key] = yield from averager() print("in grouper, after yield from, key is ", key) # 调用方 def main(data): results = {} for key, values in data.items(): # group 是调用grouper函数得到的生成器对象 group = grouper(results, key) print("\ncreate group: ", group) next(group) # 预激 group 协程。 print("pre active group ok") for value in values: # 把各个value传给grouper 传入的值最终到达averager函数中; # grouper并不知道传入的是什么,同时grouper实例在yield from处暂停 print("send to %r value %f now" % (group, value)) group.send(value) # 把None传入groupper,传入的值最终到达averager函数中,导致当前实例终止。然后继续创建下一个实例。 # 如果没有group.send(None),那么averager子生成器永远不会终止,委派生成器也永远不会在此激活,也就不会为result[key]赋值 print("send to %r none" % group) group.send(None) print("report result: ") report(results) # 输出报告 def report(results): for key, result in sorted(results.items()): group, unit = key.split(';') print('{:2} {:5} averaging {:.2f}{}'.format(result.count, group, result.average, unit)) data = { 'girls;kg': [40, 41, 42, 43, 44, 54], 'girls;m': [1.5, 1.6, 1.8, 1.5, 1.45, 1.6], 'boys;kg': [50, 51, 62, 53, 54, 54], 'boys;m': [1.6, 1.8, 1.8, 1.7, 1.55, 1.6], } if __name__ == '__main__': main(data)