yield关键字

yield关键字主要有两个使用方式:

  • yield item:会产出一个值,提供给next(...)的调用方。也可以不产生值,需要yield后面不跟任何参数,返回值为None
  • i = yield:从调用方接收数据。调用方通过.send(...)的方式将数据提供给协程。
  • yield:既不接收也不发送数据,纯粹作为流程控制使用。

yield作为协程使用时,通常也是将其作为流程控制的调度器使用。

协程

简单的例子开始说起。

 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
import inspect


def simple_coroutine2(a):
    print("-> Started: a = ", a)
    b = yield a
    print("-> Received: b = ", b)
    c = yield a + b
    print("-> Received: c = ", c)
    return c


my_cor2 = simple_coroutine2(1)
print("my_cor2 state -> ", inspect.getgeneratorstate(my_cor2))

print("\nmy_cor2 -> ", my_cor2)
print("next(my_cor2) - > ", next(my_cor2))         # 激活, 执行yield a,返回a的值。
print("my_cor2 state -> ", inspect.getgeneratorstate(my_cor2))

print("\n")
print("my_cor2.send(10) - > ", my_cor2.send(10))  # 发送10, 执行 b = yeild,打印 Recevied,执行 yield a + b, 返回 a + b 的结果,断住。
print("my_cor2 state -> ", inspect.getgeneratorstate(my_cor2))
print("\n")

try:
    print("\nmy_cor2.send(21) - > ", my_cor2.send(21))  # 产生StopIteration异常,并不会打印此内容
except StopIteration as e:
    print("cor StopIteration!")
    print("e = ", e)

print("my_cor2 state -> ", inspect.getgeneratorstate(my_cor2))

产生的结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
my_cor2 state ->  GEN_CREATED

my_cor2 ->  <generator object simple_coroutine2 at 0x7fe1668633b8>
-> Started: a =  1
next(my_cor2) - >  1
my_cor2 state ->  GEN_SUSPENDED


-> Received: b =  10
my_cor2.send(10) - >  11
my_cor2 state ->  GEN_SUSPENDED


-> Received: c =  21
cor StopIteration!
e =  21         # 将返回值当做异常捕获
my_cor2 state ->  GEN_CLOSED

yield执行顺序是:先执行yield item,再执行i = yield

所以,要想i = yield生效,需要先调用next(...),让生成器启动起来。这个过程称为预激

预激过程也可以使用装饰器来完成,可以自己定义,也可以使用系统自带的from coroutil import coroutine

另外,再最后一个i = yield接收完数据后会抛出StopIteration的异常,异常捕获后的值是原始函数的返回值,这个是正常情况。

协程有四种状态:

  • GEN_CREATED:等待开始执行状态。
  • GEN_RUNNING:解释器正在执行状态。
  • GEN_SUSPENDED:在yield表达式处挂起。
  • GEN_CLOSE:执行结束。

使用inspect.getgeneratorstate(...)函数能够捕获对应的状态。

协程的终止和异常处理

停止协程的方式还是让它产生异常。

  • 可以隐式的.send(...)一个不符合要求的数据,使其产生异常退出,如果有对应的捕获异常的函数,则能够正常结束。
  • 显式的调用throwclose方法也能结束协程。
    • throw方法是致使生成器在暂停的yield表达式处抛出指定的异常。
    • close方法是致使生成器在暂停的yield表达式处抛出GeneratorExit异常。

for循环中,如果for的对象是一个生成器的话,到最后也会抛出异常,只不过循环机制已经将异常做了相应的处理。

yield from

yield from类似于其他语言的await关键字(还没研究过这个关键字…)。

用法一:链接可迭代的对象

yield from最简单的作用就是链接可迭代的对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 下面两个函数使用的方式不同,但是结果一样
def gen0():
    for char in "ABC":
        yield char

    for num in range(7):
        yield num


print(list(gen0()))


def gen():
    yield from "ABC"
    yield from range(7)


print(list(gen()))

打印出的结果如下:

1
2
['A', 'B', 'C', 0, 1, 2, 3, 4, 5, 6]
['A', 'B', 'C', 0, 1, 2, 3, 4, 5, 6]

用法二:委派生成器

yield from还有一个更重要的用法!打开双向通道,把最外层的调用方与最内层的自生成器连接起来,两者可以直接发送和产生值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。

 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
from collections import namedtuple

Result = namedtuple("Result", "count,average")


# 子生成器
def Averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield            # 这里接收的是main函数中send的值
        if term is None:        # 这个判断是必须的,否则会一直循环下去,无法返回
            break
        total += term
        count += 1
        average = total / count
    # raise StopIteration(Result(2, 3)) # yield from会捕获StopIteration的第一个参数当做返回值
    return Result(count, average)       # 子生成器产生的值都直接传给委派生成器的调用方,即main函数


# 委派生成器
def Grouper(results, key):
    while True:
        results[key] = yield from Averager()  # yield from会捕获StopIteration的第一个参数当做返回值,即results[key]=StopIteration[0]


def Report(results):
    for key, values in results.items():
        group, unit = key.split(";")
        print("{:2} {} averaging {:.2f} {}".format(values.count, group, values.average, unit))


# 调用方
def main(data: dict):
    results = {}
    for key, values in data.items():
        group = Grouper(results, key)
        next(group)                 # 预激
        for value in values:
            group.send(value)       # 发送给自生成器的值
        group.send(None)            # 停止信号!
    Report(results)


datas = {
    "girls;kg": [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
    "girls;m": [1.6, 1.5, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
    "boys;kg": [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
    "boys;m": [1.6, 1.5, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43]
}

if __name__ == "__main__":
    main(datas)

# 输出结果如下。
>> 10 girls averaging 42.04 kg
>> 10 girls averaging 1.43 m
>> 10 boys averaging 42.04 kg
>> 10 boys averaging 1.43 m

在上述例子中,调用方main函数内部通过参数group定义为生成器,在委派生成器grouper()中使用yield from Averager(...)时,子生成器Averager()会获得控制权,把生成的值传给委派生成器grouper()的调用方main函数,即main()直接和Averager()接触。与此同时委派生成器grouper()将会阻塞,直到子生成器Averager()终止。

16-yield-from.png

整个过程中,委派生成器只做中间管道的部分。