0x01 yield与生成器

yield与协程

Python中, 可以通过使用yield定义的生成器, 实现协程.

生成器

协程生成器的基本行为

首先定义一个生成器:

def simple_coroutine():
    print('-> coroutine started')
    x = yield
    print('-> coroutine received:', x)

形式如同定义函数, 如果定义体中含有yield关键字, 则就是一个生成器的定义体. 创建一个生成器对象:

my_coro = simple_coroutine()
my_coro

输出为:

<generator object simple_coroutine at 0x00000267ED4245C8>

协程有四种状态:

  • GEN_CREATED: 等待开始执行

  • GEN_RUNNING: 解释器正在执行

  • GEN_SUSPENDED: 在yield表达式处暂停

  • GEN_CLOSED: 执行结束

可以使用inspect包中的inspect.getgeneratorstate函数查看某个协程的状态:

import inspect

inspect.getgeneratorstate(my_coro)

输出为:

GEN_CREATED

一个刚创建的生成器, 其状态为GEN_CREATED, 此时的协程还没有被激活, 需要经过预激(prime)操作, 让协程向前执行到第一个yield表达式, 准备好作为活跃的协程使用. 预激的方法有两种:

  • 调用next(my_coro)方法

  • 使用协程的send方法, 并传递一个None, my_coro.send(None)

my_coro.send(None)

输出为:

-> coroutine started

可以看到, 此时的生成器执行到yield的表达式处, 并将控制权交还给了调用方.

查看此时协程的状态:

inspect.getgeneratorstate(my_coro)

输出为:

GEN_SUSPENDED

即状态变成了GEN_SUSPENDED, 代表在yield表达式处暂停.

继续调用:

my_coro.send(42)

输出为:

-> coroutine received: 42
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-12-7c96f97a77cb> in <module>()
----> 1 my_coro.send(42)

StopIteration:

可以看出:

  • send方法输入的参数, 会被赋给yield表达式所在行的左侧的变量, 此处将42赋给了参数x

  • 生成器执行完之后, 抛出StopIteration错误. 可以由调用方捕捉这个错误, 进行相应逻辑的处理

查看此时协程的状态:

inspect.getgeneratorstate(my_coro)

输出为:

GEN_CLOSED

生成器, 也即协程已经正常结束了.

如果协程没有预激, 就直接使用send方法传递参数, 会报如下的错位:

my_coro1 = simple_coroutine()
print(inspect.getgeneratorstate(my_coro))
my_coro1.send(42)

输出为:

GEN_CLOSED
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-14-c1f2b4df7ce8> in <module>()
      1 my_coro1 = simple_coroutine()
      2 print(inspect.getgeneratorstate(my_coro))
----> 3 my_coro1.send(42)

TypeError: can't send non-None value to a just-started generator

协程向调用方返回结果

生成器在执行到yield表达式暂停时, 会将yield表达式执行后的结果, 返回给调用方. 看下面的例子:

def simple_coro2(a):
    print('-> coroutine started: a =', a)
    b = yield a  # 返回a的值
    print('-> Received: b =', b)
    c = yield a + b  # 返回a+b的值
    print('-> Received: c =', c)

my_coro2 = simple_coro2(14)  # 创建生成器
print(inspect.getgeneratorstate(my_coro2))
next(my_coro2)  # 激活生成器

输出为:

GEN_CREATED
-> coroutine started: a = 14
14

可以看到激活操作, 执行了第一个yield表达式, 将生成器中a的值14返回给了调用方, 并等待send操作传入值. 继续调用send方法, 会将send中指定的参数传递给b, 向下执行, 将yield表达式a+b的值返回:

my_coro2.send(28)

输出为:

-> Received: b = 28
42

结果如同预期, 将a+b=42的结果返回. 继续调用, 协程执行完毕.

my_coro2.send(99)

输出为

-> Received: c = 99
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-18-977093b924ab> in <module>()
----> 1 my_coro2.send(99)

StopIteration:

整个过程分三步:

  • 调用next(my_coro2), 打印a的值, 然后执行yield a, 产出数字14, 返回

  • 调用my_coro2.send(28), 把28赋值给b, 打印b的值, 然后执行 yield a + b, 产生数字42, 返回

  • 调用my_coro2.send(99), 把99赋值给c, 打印c的值, 产生数字99, 返回

使用装饰器预激协程

新创建的生成器对象, 每次都要手动进行预激, 比较麻烦, 可以用装饰器进行预激操作:

from functools import wraps

def coroutinue(func):
    '''
    装饰器: 向前执行到第一个`yield`表达式,预激`func`
    :param func: func name
    :return: primer
    '''
    @wraps(func)
    def primer(*args, **kwargs):
        # 把装饰器生成器函数替换成这里的primer函数;调用primer函数时,返回预激后的生成器。
        gen = func(*args, **kwargs)
        # 调用被被装饰函数,获取生成器对象
        next(gen)  # 预激生成器
        return gen  # 返回生成器
    return primer

定义生成器:

@coroutinue
def simple_coro(a):
    a = yield

直接创建的生成器对象就已经是激活的状态:

coro = simple_coro(12)
inspect.getgeneratorstate(coro)

输出为:

GEN_SUSPENDED

终止协程和异常处理

协程向调用方

协程运行过程中的异常, 会向上冒泡, 传递给next函数或者send方法的调用方, 调用方如果没有对其进行处理, 就会导致错误终止.

@coroutinue
def averager():
    # 使用协程求平均值
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average
        total += term
        count += 1
        average = total/count
coro_avg = averager()
print(coro_avg.send(40))
print(coro_avg.send(50))
print(coro_avg.send('123')) # 由于发送的不是数字,导致内部有异常抛出。

结果为:

40.0
45.0
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-23-0aa2c396e8a8> in <module>()
      2 print(coro_avg.send(40))
      3 print(coro_avg.send(50))
----> 4 print(coro_avg.send('123')) # 由于发送的不是数字,导致内部有异常抛出。

<ipython-input-22-f96454dd016a> in averager()
      7     while True:
      8         term = yield average
----> 9         total += term
     10         count += 1
     11         average = total/count

TypeError: unsupported operand type(s) for +=: 'float' and 'str'

调用方向协程

上面的例子, 换个角度, 可以看做是传入特别的参数, 手动使协程退出. 而生成器中有两个特别的方法可以实现这种事情.

  • throw方法

    generator.throw(exc_type[, exc_value[, traceback]]), 这个方法使生成器在暂停的yield表达式处抛出指定的异常, 如果生成器处理了抛出的异常, 代码会继续向下进行, 直到遇到下一个yield表达式或整个代码运行完毕. 当然, 如同send方法一样, 下一个yield产出的值会成为调用throw方法得到的返回值. 如果没有处理, 则向上冒泡, 直接抛出

  • close方法

    generator.close(), 生成器在暂停的yield表达式处抛出GeneratorExit异常, 如果生成器没有处理这个异常或者抛出了StopIteration异常, 调用方不会报错, 如果收到GeneratorExit异常, 生成器一定不能产出值, 否则解释器会抛出RuntimeError异常.

class DemoException(Exception):
    pass
@coroutinue
def exc_handling():
    print('-> coroutine started')
    while True:
        try:
            x = yield
        except DemoException:
            print('*** DemoException handled. Conginuing...')
        else:
            # 如果没有异常显示接收到的值
            print('--> coroutine received: {!r}'.format(x))
    raise RuntimeError('This line should never run.')  # 这一行永远不会执行
exc_coro = exc_handling()

exc_coro.send(11)
exc_coro.send(12)
exc_coro.send(13)
exc_coro.close()

输出为:

-> coroutine started
--> coroutine received: 11
--> coroutine received: 12
--> coroutine received: 13

查看当前状态:

inspect.getgeneratorstate(exc_coro)

结果为:

GEN_CLOSED

如果传入DemoException, 因为做了异常处理, 协程不会中止:

exc_coro = exc_handling()

exc_coro.send(11)
exc_coro.send(12)
exc_coro.send(13)
exc_coro.throw(DemoException) # 协程不会中止,但是如果传入的是未处理的异常,协程会终止

结果为:

-> coroutine started
--> coroutine received: 11
--> coroutine received: 12
--> coroutine received: 13
*** DemoException handled. Conginuing...

查看当前状态:

inspect.getgeneratorstate(exc_coro)

结果为:

GEN_SUSPENDED

协程return

由于协程执行完毕后向外报StopIteration错误, 对于有return值的协程生成器, 如何获取它return的值, 是需要考察的问题.

from collections import namedtuple

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

def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield
        if term is None:
            break  # 为了返回值,协程必须正常终止;这里是退出条件
        total += term
        count += 1
        average = total/count    # 返回一个namedtuple,包含count和average两个字段。在python3.3前,如果生成器返回值,会报错
    return Result(count, average)
coro_avg = averager()
next(coro_avg)
coro_avg.send(20) # 并没有返回值
coro_avg.send(30)
coro_avg.send(40)
# 发送None终止循环,导致协程结束。生成器对象会抛出StopIteration异常。异常对象的value属性保存着返回值。
coro_avg.send(None)

结果为:

---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-32-e7656e6f4153> in <module>()
      5 coro_avg.send(40)
      6 # 发送None终止循环,导致协程结束。生成器对象会抛出StopIteration异常。异常对象的value属性保存着返回值。
----> 7 coro_avg.send(None)

StopIteration: Result(count=3, average=30.0)

可以看到奇特的一点, return表达式的值, 会作为StopIteration异常的一个属性值被带出, 这样做是为了保留生成器对象耗尽时抛出StopIteration异常的行为. 如果要获取这个值, 就需要如下的操作:

coro_avg = averager()
next(coro_avg)
coro_avg.send(20) # 并没有返回值
coro_avg.send(30)
coro_avg.send(40)
try:
    coro_avg.send(None)
except StopIteration as exc:
    result = exc.value
result

就能得到结果:

Result(count=3, average=30.0)

参考资料

python协程1:协程 10分钟入门

python协程2:yield from 分析

最后更新于

这有帮助吗?