Python协程与并发——yield

首先要说明yield的中文释义,“产出;生产”。和return不同,return是返回,即我们调用一次,就返回一次,而生成器函数的yield可以不断的生成值。
举个不恰当的例子,函数返回一个值可以看做是一枚鸡蛋,经过一定的过程,只能孵化出一只小鸡。生成器函数内的yield更像一只母鸡(生成器对象),不断地下蛋(产出结果)。

先看Python2的用法

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
In [1]: def get_generator(n):
...: for i in range(n):
...: result = yield (i, i + 1, )
...: print('result is ', result)
...:

In [2]: my_generator = get_generator(5)

In [3]: item = my_generator.next()

In [4]: print(item)
(0, 1)

In [5]: item = my_generator.next()
('result is ', None)

In [6]: print(item)
(1, 2)

In [7]: item = my_generator.send('a')
('result is ', 'a')

In [8]: print(item)
(2, 3)

In [9]: item = my_generator.send('b')
('result is ', 'b')

In [10]: item = my_generator.next()
('result is ', None)

In [11]: item = my_generator.next()
('result is ', None)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-11-5d4475abe640> in <module>()
----> 1 item = my_generator.next()

StopIteration:

yield仅在定义生成器函数时使用,并且需要在生成器函数的函数体内使用。如示例中的get_generator函数即为生成器函数。

调用生成器函数时,会返回一个叫做生成器的迭代器。这里需要意识到生成器/迭代器是一个对象。如示例中my_generator = get_generator(5)不会执行该函数体内的语句,而是返回了一个生成器对象,赋值给my_generator

执行生成器对象的方法时(next, send, throw, close下面会详细说明),生成器函数体内的代码才会执行。如示例中的第3次输入In [3]: item = my_generator.next(),将使get_generator函数开始执行。

当执行到yield语句时,yield语句中的expression_list(表达式列表)将作为产出值返回给调用者。如示例中的(i, i + 1, )将作为my_generator.next()的返回结果,最终赋值给item
同时生成器函数在这里挂起/冻结挂起是指保留了所有本地状态,包括局部变量,指令指针和内部评估堆栈的绑定关系等,如示例中的局部变量i),如示例中的result = yield (i, i + 1, ),只会执行yield (i, i + 1, ),而不会将yield表达式的值赋值给resultyield表达式的值由下一次调用决定,现在还不存在)。

再次调用生成器对象的方法时,生成器函数会从挂起的地方继续执行,如示例中第5次输入In [5]: item = my_generator.next()next()方法会使生成器函数内上一次挂起的yield表达式值为None,然后执行result =的赋值操作。
yield语句的执行就像是发起了一次外部调用,会保留函数体内部状态,跳出当前生成器函数,执行外部代码,执行完毕后,继续执行函数体内部的代码。

生成器函数恢复执行时使用的生成器对象方法,决定了yield表达式的值,如示例中第5、7次输入In [5]: item = my_generator.next()In [7]: item = my_generator.send('a')决定了yield (i, i + 1, )表达式的值,分别为None'a'(后面详细介绍nextsend方法)。

这些特点使得生成器功能与协程非常相似,可以多次yield,有多个入口,执行可被暂停。唯一的区别是生成器函数无法控制yield后继续执行的位置,总是生成器的调用者来进行控制。

生成器的方法

介绍生成器方法之前,我们先解释一下当前yield表达式。生成器函数恢复执行时,从上一次挂起的yield表达式的位置执行,这个yield表达式叫做当前yield表达式,所以在第一次执行生成器函数时,由于不存在上一次挂起,因此不会有当前yield表达式的概念,当前yield表达式只存在于再次恢复执行中。

生成器的方法可以控制生成器函数的执行,但在生成器对象执行过程中调用这些方法,将会发起ValueError异常。

  • generator.next()

用于启动和恢复执行生成器函数。

使用next()方法恢复执行时,会使当前yield表达式的值为None,并返回下一个yield表达式列表的值。
如示例中第5次输入In [5]: item = my_generator.next()时,首先从上一次yield表达式挂起的位置执行,使yield (i, i + 1, )表达式值为None,然后赋值给result。然后执行print,进入下一轮循环,再次yield时,表达式列表(i, i + 1, )才会返回给当前的next()函数。

如果生成器并非以yield形式退出,那么调用next()方法就会引起StopIteration异常,如第11次输入。

  • generator.send(value)

用于生成器函数的恢复执行,并传递值。

value参数会成为当前yield表达式的值。如示例中第7次输入In [7]: item = my_generator.send('a'),使当前yield表达式值为'a'

send()方法也会返回下一次yield产出的表达式列表,或者在生成器函数没有产出值时,也引起StopIteration异常。

如果用send()方法启动生成器函数的话,那么value参数必须为None。因为第一次执行时没有当前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
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
In [1]: def echo(value=None):
...: try:
...: while True:
...: yield value
...: except TypeError:
...: yield 'type_error'
...: except GeneratorExit:
...: print('GeneratorExit')
...: if value == 3:
...: yield 3
...: else:
...: raise GeneratorExit
...:

In [2]: g1 = echo(1)

In [3]: print(g1.next())
1

In [4]: print(g1.throw(TypeError))
type_error

In [5]: print(g1.next())
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-5-ab3315bb99df> in <module>()
----> 1 print(g1.next())

StopIteration:

In [6]: g2 = echo(2)

In [7]: print(g2.next())
2

In [8]: print(g2.close())
GeneratorExit
None

In [9]: print(g2.next())
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-9-6591d093f5cb> in <module>()
----> 1 print(g2.next())

StopIteration:

In [10]: print(g2.close())
None

In [11]: g3 = echo(3)

In [12]: print(g3.next())
3

In [13]: print(g3.close())
GeneratorExit
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
<ipython-input-12-64ded7703842> in <module>()
----> 1 print(g3.close())

RuntimeError: generator ignored GeneratorExit

我们用上面的例子来讲解throwclose方法。

  • generator.throw(type[, value[, traceback]])

在生成器挂起的位置发起type类型的异常,然后返回下一次生成器产出的值,如果没有产出值则发起StopIteration异常。

如示例第2-5次输入中第4次输入In [4]: print(g1.throw(TypeError)),会在上一次挂起的位置即yield value处发起TypeError异常,然后被except TypeError:捕获,然后执行yield 'type_error',产出值返回给调用者,所以第4次输入会打印出type_error

如果生成器函数没有捕捉到传入的异常,或者发起了其他异常,那么该异常会向上抛出至调用者。

  • generator.close()

在生成器挂起的位置发起GeneratorExit异常。

如果生成器函数发起StopIteration(生成器函数执行完成或执行close()方法都会使生成器结束,再次获取产出值时会发起StopIteration异常)或GeneratorExit(当该异常未被捕获时)异常,那么生成器就会关闭,然后返回至调用者,并且代码不会发起异常。

如上面示例的第6-10次输入。In [6]: g2 = echo(2)创建了一个生成器。In [7]: print(g2.next())执行生成器,并在yield value处挂起。
In [8]: print(g2.close())调用close()方法,这时会在上次挂起,即yield value处发起GeneratorExit异常,所以被except GeneratorExit:捕捉到,然后生成器函数执行print('GeneratorExit'),我们可以看到输出。
生成器传入的参数为2,所以接下来会执行raise GeneratorExit,从执行结果来看,close()方法触发生成器函数发起的GeneratorExit,不会传递至调用者,也没有返回值。
继续获取生成器产出值In [9]: print(g2.next()),会发起StopIteration,证明生成器已经被停止了。
In [10]: print(g2.close())继续执行close()方法,会在生成器挂起的位置继续执行,但此时生成器已经停止了,会发起StopIteration异常,示例中输出None,表明close()方法触发生成器函数发起的StopIteration异常,不会传递至调用者,也没有返回值。

已关闭的生成器继续返回产出值会发起RuntimeError异常。如示例中第10-13次输入。close()方法会被except GeneratorExit:捕获,根据逻辑会执行yield 3语句,此时报错。

生成器内发起的其他异常会传递至调用者。如果生成器由于异常结束,或执行完毕结束时,调用close()不会有任何影响。

先看Python3的用法

这里只说相比Python2新增的内容。

python3中可以使用yield from expression表达式,并且除了可以在定义生成器函数时使用yield表达式,也可以在定义异步生成器函数(async def定义的函数体内使用yield)时使用。

生成器函数

try结构中可以使用yield表达式。如果生成器在结束(引用计数为0或被垃圾回收)之前没有恢复执行,那么生成器的close()方法将被调用,并执行finally语句。

yield from <expr>语句将表达式当做子迭代器使用。所有子迭代器产出的值,都会传递给当前生成器方法的调用者。
send()方法传入的值、throw()传入的异常都会传递至底层的迭代器(子迭代器),前提是底层的迭代器有对应的合适的方法,否则,send()方法会引起AttributeErrorTypeError异常,throw()方法会立即抛出传入的异常。

底层迭代器完成之后,发起的StopIteration对象的value属性就会成为当前yield表达式的值。yield表达式的值可以由StopIteration异常精确设置,也可以在子迭代器是生成器时(子生成器会产出值)自动设置。

异步生成器函数