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 句法,使用它可以把复杂的生成器重构成小型的嵌套生成器,省去了之前把生成器的工作委托给子生成器所需的大量样板代码。
1
2
3
4
5
6
7
8
9
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)。为了简化协程的用法,有时会使用一个预激装饰器。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 句法调用协程时,会自动预激。

1
2
3
4
5
6
7
8
9
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 异常。生成器抛出的其他异常会向上冒泡,传给调用方。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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中取消了这一限制,因而允许协程可以返回值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 异常。如果需要接收返回值,可以这样:

1
2
3
4
5
6
7
8

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 表达式。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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 表达式的生成器函数;

子生成器:从 yield from 表达式中 部分获取的生成器;

调用方:调用委派生成器的客户端代码;

下图是这三者之间的交互关系:

委派生成器在 yield from 表达式处暂停时,调用方可以直接把数据发给子生成器,子生成器再把产出的值发给调用方。子生成器返回之后,解释器会抛出StopIteration 异常,并把返回值附加到异常对象上,此时委派生成器会恢复。

下面是一个求平均身高和体重的示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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)