异步-协程-yield in Python, 2022-06-13

(2022.06.13 Mon)
协程往往和线程做对比。协程也是并发的一种,协程与线程不同之处在于:

  • 线程的调度由CPU执行,协程的调用由开发者写的函数执行
  • 协程更轻量级
  • 协程运行与同一个线程中

在Python中协程可由生成器实现,某种程度上可以作为流程控制工具的yield也可以实现协作式多任务,似乎是为协程而设计。

生成器的基本行为

首先定义一个生成器函数。

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

simple_coroutine实例,得到一个生成器对象

>> a = simple_coroutine()
>> a

首先回忆生成器的使用方法。yield关键字右边的部分如果为空,则该yield关键字只接受用户传入的值,不返回值给用户,或返回Noneyield右边的变量是每次通过next方法调用生成器时返回给用户的变量。yield左边赋值的变量,即x = yield,表示用户在通过send方法调用生成器时传入的值将会保存在变量x中。

通过next函数调用生成器,并用send方法传入数值到生成器中。

>> next(a)
-> coroutine started
>> a.send(998)
-> coroutine received: 998
Traceback (most recent call last):
  File "", line 1, in 
StopIteration

(2022.06.14 Tues)
上面的例子yield的等号左边被赋值,也就是caller可以传递值进生成器,另有一种不赋值,不能传值进生成器的情况。比如用yield产生num到100间所有的奇数的方式。

def generator_odd(num):
    if num > 100:
        num = num % 100
    for i in range(num, 101):
        if i%2 == 1:
            yield i

调用

>> geno = generator_odd(83)
>> next(geno)
83
>> type(geno)

>> list(geno)
[85, 87, 89, 91, 93, 95, 97, 99]

协程的状态

协程分为四种状态,通过inspect.getgeneratorstate可查询生成器处在协程的哪种状态。四种状态分别是

  • GEN_CREATED:生成器等待开始执行,创建之后
  • GEN_RUNNING:生成器解释执行中
  • GEN_SUSPENDED:在表达式处暂停
  • GEN_CLOSE:执行结束
>> from inspect import getgeneratorstate
>> getgeneratorstate(a)
'GEN_CLOSED'

协程的预激(prime)

在生成器的第一次调用时,如果使用send方法传入一个非None到生成器中,则会返回TypeError错误。

>> a = simple_coroutine()
>> a.send(10)
Traceback (most recent call last):
  File "", line 1, in 
TypeError: can't send non-None value to a just-started coroutine

通常,在一个生成器第一步使用时,调用next方法,用以预激协程,也就是让协程向前执行到第一个yield表达式,准备好作为活跃的协程使用。

预激的方法有两种:

  • 调用next方法
  • 调用send方法传入None

为简化预激协程的过程,可使用装饰器对生成器函数进行装饰。

>> def coroutine(func):
       @wraps(func)
       def primer(*args, **kwargs):
           gen = func(*args, **kwargs) # 调用被装饰的函数,获取生成器对象
           next(gen) # 预激生成器
           return gen # 返回生成器
       return primer

用该协程函数,装饰计算平均值的生成器。

@coroutine
def average():
    total, cntr, average = 0, 0, None
    while True:
        tmp = yield average
        total += tmp
        cntr += 1
        average = total/cntr

调用average函数创建一个生成器对象,注意因为该函数已经被coroutine装饰,已经完成了预激,在生成器对象创建之后可直接向其中传入值,而不必预激

>> b = average()
>> type(b)

>> inspect.getgeneratorstate(b)
'GEN_SUSPENDED'
>> b.send(1000)
1000.0

注意到,在创建生成器对象后,查看生成器的状态,因为装饰器中已经完成了预激,所以状态为GEN_SUSPENDED。作为对比,查看一个为被装饰的生成器返回的状态是'GEN_CREATED'。

def noprime():
    c = 1
    tmp = yield c
    c += 1
>> np = noprime()
>> inspect.getgeneratorstate(np)
'GEN_CREATED'

协程的执行顺序

有这样一个生成器函数

def simple_coro2(a):
    print('-> Started: a =', a)
    b = yield a
    print('-> Received: b =', b)
    c = yield a + b
    print('-> Received: c =', c)

运行结果如下

>> d = simple_coro2(5)
>> next(d) # 1
-> Started: a = 5 # 2
5 # 3
>> d.send(99) 
-> Received: b = 99 # 4
104 # 5 
>> d.send(25)
-> Received: c = 25 # 6 
Traceback (most recent call last):
  File "", line 1, in 
StopIteration

# 1:预激,运行代码到第一个yield处,于是有了# 2和# 3
# 2:打印第一个yield之前的print指令
# 3:返回yield a中的a
# 4和# 5:从第一个b = yield a的等号左边开始运行,到下一个c = yield a + b的等号右边,于是有了# 5的104,因a+b=104
# 6:运行c = yield a + b的等号左边,接受了来自用户的send赋值,并运行到生成器结尾,以StopIteration退出

从上面的过程可以看到,对于能接受用户赋值的yield表达式,每次运行到yield表达式的等号右侧,下次调用nextsendyield的表达式左侧开始运行,顺序如图所示

Coroutine running order

终止协程和异常处理

继续考虑求平均值的生成器

@coroutine
def average():
    total, cntr, average = 0, 0, None
    while True:
        tmp = yield average
        total += tmp
        cntr += 1
        average = total/cntr

创建生成器,并传递不符合要求的类型

>> f = average()
>> getgeneratorstate(f)
'GEN_SUSPENDED'
>> f.send('s')
Traceback (most recent call last):
  File "", line 1, in 
  File "", line 6, in average
TypeError: unsupported operand type(s) for +=: 'int' and 'str'
>> f.send(5)
Traceback (most recent call last):
  File "", line 1, in 
StopIteration

这个案例给出了一种终止协程的方式:发送哨符值,比如None。使用生成器对象的close方法可以关闭协程。

致使生成器在暂停的yield表达式处抛出指定的异常。如果生成
器处理了抛出的异常,代码会向前执行到下一个 yield 表达式,而产
出的值会成为调用 generator.throw 方法得到的返回值。如果生成器
没有处理抛出的异常,异常会向上冒泡,传到调用方的上下文中。

协程的异常处理,可参考下面例子

class DemoException(Exception):
    pass

def demo_exc_handling():
    print('-> coroutine started')
    while True:
        try:
            x = yield
        except DemoException:
            print('*** DemoException handled. Continuing...')
        else: 
            print('-> coroutine received: {!r}'.format(x))

创建生成器对象,并用throw方法抛出异常

>> de = demo_exc_handing()
>> de.send(None)
-> coroutine started
>> de.throw(DemoException) # 1
** DemoException handled. Continuing...
>> de.send(2)  # 2
-> coroutine received: 2

# 1:抛出DemoException异常
# 2:上一步抛出异常后,协程并没有终止

(2022.06.14 Tues)

让协程返回值

协程返回值最简单的方法是当检测到caller通过send传入的值为特定值,如None时,跳出生成器的循环条件,并返回指定的值。考虑计算平均值的例子。

def average():
    total, count, average = 0, 0, None
    while True:
        term = yield
        if term is None: break
        total += term
        count += 1
        average = total / count
    return (count, average)

调用得到

>> f = average()
>> f.send(None)
>> f.send(1)
>> f.send(3)
>> f.send(10)
>> f.send(None)
Traceback (most recent call last):
  File "", line 1, in 
StopIteration: (3, 4.666666666666667)

注意到生成器函数的返回结果,即return (count, average)部分,是StopIteration的属性,所以可以在调用时catch这个异常,并调用该属性。

>> f = average()
>> f.send(None) # prime coroutine
>> f.send(10)
>> f.send(20)
>> try:
...     f.send(None)
... except StopIteration as s:
...     tmp = s.value
... 
>> tmp
(2, 15.0)

yield from

yield from语法出现在Python 3.3之后,其后接应的对象是可迭代对象,迭代器,和生成器,用于简化for循环中的yield表达式。仅使用yield指令的情况下有如下表达:

def gen():
    for c in 'AB':
        yield c
    for i in range(1, 3):
        yield i

调用

>> list(gen())
['A', 'B', 1, 2]

如果使用yield from表达,gen方法可以简化为下面这种形式,且调用结果完全相同。

def gen():
    yield from 'AB'
    yield from range(1, 3)

yield from加可迭代对象,可以把可迭代对象中的每个元素一一yield出来。

yield from后面接上生成器,就得到了生成器的嵌套。尽管生成器的嵌套并非一定要用yield from方法,但这个方法可以避免不必要的麻烦。

研究生成器的嵌套前首先明确几个概念:

  1. 调用方:调⽤委派⽣成器的客户端(调⽤⽅)代码
  2. 委托生成器:包含yield from表达式的⽣成器函数
  3. 子生成器:yield from后⾯加的⽣成器函数

下面用求平均的案例来解释上面几个概念

# 子生成器
def average():
    total, cntr, average = 0, 0, None
    while True:
        tmp = yield average
        total += tmp
        cntr += 1
        average = total/cntr
# 委托生成器
def proxy_gen():
    while True:
        yield from average()
# 调用方
def main():
    calc_average = proxy_gen()
    next(calc_average) # 预激生成器
    print(calc_average.send(10))  # 10.0
    print(calc_average.send(20))  # 15.0
    print(calc_average.send(30))  # 20.0

委托生成器的作用是在调用方和子生成器之间建立一个双向通道。

下面查看一下当出现异常时,对委托生成器做修改后如何处理异常。

def average_gen():
    total =0
    count =0
    average =0
    while True:
        new_num = yield average
        if new_num is None:
            break
        count += 1
        total += new_num
        average = total/count
    return total, count, average
def proxy_gen():
    while True:
    # 只有⼦⽣成器要结束(return)了,yield from左边的变量才会被赋值,后⾯的代码才会执⾏。
        total, count, average = yield from average_gen()
        print("计算完毕\n 总共传⼊{} 个数值,总和:{},平均数:{}".format(count, total, average))
def main():
    calc_average = proxy_gen()
    next(calc_average)
    print(calc_average.send(10))  #
    print(calc_average.send(20))  #
    print(calc_average.send(30))  #
    calc_average.send(None)

返回结果

>> main()
10.0
15.0
20.0
计算完毕!!
总共传⼊3 个数值,总和:60,平均数:20.0

如果避开委托生成器,直接对子生成器发送值和None,则最后返回StopIteration和对应的total等值。使用了委托生成器则能优雅的处理异常并返回打印结果。

>>> a = average_gen()
>>> a.send(None)
0
>>> a.send(10)
10.0
>>> a.send(20)
15.0
>>> a.send(40)
23.333333333333332
>>> a.send(None)
Traceback (most recent call last):
  File "", line 1, in 
StopIteration: (70, 3, 23.333333333333332)

当然委托生成器不只是处理异常,StopIteration异常处理完全可以自己手写完成。委托生成器做了更多。

下面的伪代码,等效于委派生成器中的RESULT = yield from EXPR语句。以下代码过于复杂,可以简单的理解为yield from帮忙做了很多的异常处理。

_i = iter(EXPR) 
try:
    _y = next(_i) 
except StopIteration as _e:
    _r = _e.value 
else:
    while 1: 
        try:
            _s = yield _y 
        except GeneratorExit as _e: 
            try:
                _m = _i.close
            except AttributeError:
                pass
            else:
                _m()
            raise _e
        except BaseException as _e: 
            _x = sys.exc_info()
            try:
                _m = _i.throw
            except AttributeError:
                raise _e
            else: 
                try:
                    _y = _m(*_x)
                except StopIteration as _e:
                    _r = _e.value
                    break
        else: 
            try: 
                if _s is None: 
                    _y = next(_i)
                else:
                    _y = _i.send(_s)
            except StopIteration as _e: 
                _r = _e.value
                break
RESULT = _r 

以上代码的说明如下:

  1. 迭代器(即可指⼦⽣成器)产⽣的值直接返还给调⽤者
  2. 任何使⽤send()⽅法发给委派⽣产器(即外部⽣产器)的值被直接传递给迭代器。如果send值是None,则调⽤迭代器next()⽅法;如果不为None,则调⽤迭代器的send()⽅法。如果对迭代器的调⽤产⽣StopIteration异常,委派⽣产器恢复继续执⾏yield from后⾯的语句;若迭代器产⽣其他任何异常,则都传递给委派⽣产器。
  3. ⼦⽣成器可能只是⼀个迭代器,并不是⼀个作为协程的⽣成器,所以它不⽀持throw()close()⽅法,即可能会产⽣AttributeError异常。
  4. 除了GeneratorExit异常外的其他抛给委派⽣产器的异常,将会被传递到迭代器的throw()⽅法。如果迭代器throw()调⽤产⽣了StopIteration异常,委派⽣产器恢复并继续执⾏,其他异常则传递给委派⽣产器。
  1. 如果GeneratorExit异常被抛给委派⽣产器,或者委派⽣产器的close()⽅法被调⽤,如果迭代器有close()的话也将被调⽤。如果close()调⽤产⽣异常,异常将传递给委派⽣产器。否则,委派⽣产器将抛出GeneratorExit异常。
  2. 当迭代器结束并抛出异常时,yield from表达式的值是其StopIteration异常中的第⼀个参数。
  3. ⼀个⽣成器中的return expr语句将会从⽣成器退出并抛出StopIteration(expr)异常。

Reference

1 流畅的Python,Luciano R. 著,安道等译,中国工信出版社,人民邮电出版社
2 百度文库-Python并发编程之深入理解yieldfrom语法(八)

你可能感兴趣的:(异步-协程-yield in Python, 2022-06-13)