流畅的Python: 协程(基础)


1. 用作协程的生成器的基本行为
def simple_coroutine():
    print("—> Coroutines started")
    var = yield   # 协程使用生成器函数定义:定义体中有yield关键字。
    print(f"—> Coroutines received {var}")
    
cor = simple_coroutine()

cor
Out[4]: <generator object simple_coroutine at 0x000001D368E8F410>
  
next(cor)  # 激活协程> Coroutines started

cor.send(123)> Coroutines received 123
Traceback (most recent call last):
  File "D:\Python3.6.0\lib\site-packages\IPython\core\interactiveshell.py", line 2961, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-6-3cd430c98ae0>", line 1, in <module>
    cor.send(123)
StopIteration

var = yield:yield在表达式中使用;如果协程只需从客户那里接收数据,那么产出的值是None——这个值是隐式指定的,因为yield关键字右边没有表达式。首先要调用 next(...) 函数,因为生成器还没启动,没在yield语句处暂停,所以一开始无法发送数据

协程可以身处四个状态中的一个。当前状态可以使用 inspect.getgeneratorstate(...) 函数确定,该函数会返回下述字符串中的一个:

  • ‘GEN_CREATED’:等待开始执行;
  • ‘GEN_RUNNING’:解释器正在执行;
  • ‘GEN_SUSPENDED’:在yield表达式处暂停;
  • ‘GEN_CLOSED’:执行结束;

因为send方法的参数会成为暂停的yield表达式的值,所以,仅当协程处于暂停状态时才能调用send方法,例如cor.send(123)。不过,如果协程还没激活(即,状态是’GEN_CREATED’),情况就不同了。因此,始终要调用next(cor)激活协程——也可以调用cor.send(None),效果一样。

cor = simple_coroutine()
cor.send(None)  # 激活协程

# <generator object simple_coroutine at 0x000002601BE2E200>
# Coroutines started

如果创建协程对象后立即把None之外的值发给它,会出现下述错误:can’t send non-None value to a just-started generator.

cor = simple_coroutine()
cor.send(123)

Traceback (most recent call last):
  File "D:\Python3.6.0\lib\site-packages\IPython\core\interactiveshell.py", line 2961, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-12-3cd430c98ae0>", line 1, in <module>
    cor.send(123)
TypeError: can't send non-None value to a just-started generator

最先调用 next(cor) 函数这一步通常称为“预激”(prime)协程(即,让协程向前执行到第一个yield表达式,准备好作为活跃的协程使用)。


def simple_coroutine2(a):
    print(f"——> started a:{a}")
    b = yield a
    print(f"——> receiced b:{b}")
    c = yield a + b
    print(f"——> receiced c:{c}")

cor2 = simple_coroutine2(1)
print(getgeneratorstate(cor2))  # GEN_CREATED  协程未启动

cor2.send(None)
print(getgeneratorstate(cor2))  # GEN_SUSPENDED

print(cor2.send(2))
print(cor2.send(10))

'''
GEN_CREATED
——> started a:1
GEN_SUSPENDED
——> receiced b:2
3
——> receiced c:10
Traceback (most recent call last):
  File ".../a5_3_demo_coroutine.py", line 29, in <module>
    print(cor2.send(10))
StopIteration
'''

协程在yield关键字所在的位置暂停执行。在赋值语句中,=右边的代码在赋值之前执行。因此,对于b=yield a 这行代码来说,等到客户端代码再激活协程时才会设定b的值。

simple_coroutine2 协程的执行过程分为3个阶段:各个阶段都在yield表达式中结束,而且下一个阶段都从那一行代码开始,然后再把yield表达式的值赋给变量。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hwFdvWVj-1583855700096)(../../../markdown_pic/book2_coroutine_demo.png)]


2. 使用协程计算移动平均值
def average():
    total, count = 0, 0
    average = None
    while True:
        var = yield average
        total += var
        count += 1
        average = total / count

avg = average()
avg.send(None)  # 预激协程

print(avg.send(1))
print(avg.send(3))
print(avg.send(5))

无限循环表明,只要调用方不断把值发给这个协程,它就会一直接收值,然后生成结果。仅当调用方在协程上调用.close()方法,或者没有对协程的引用而被垃圾回收程序回收时,这个协程才会终止。这里的yield表达式用于暂停执行协程,把结果发给调用方;还用于接收调用方后面发给协程的值,恢复无限循环。


3. 预激协程的装饰器

使用协程之前必须预激,可是这一步容易忘记。为了避免忘记,可以在协程上使用一个特殊的装饰器。

如果不预激,那么协程没什么用。调用my_coro.send(x)之前,记住一定要调用 next(my_coro) 或者my_coro.send(None) 。为了简化协程的用法,有时会使用一个预激装饰器。

from functools import wraps

def coroutine(func):
    # 装饰器:向前执行到第一个yield表达式,预激func
    @wraps(func)
    def primer(*args, **kwargs):
        gen = func(*args, **kwargs)
        next(gen)
        return gen
    return primer

@coroutine
def average_primer():
    total, count = 0, 0
    average = None
    while True:
        var = yield average
        total += var
        count += 1
        average = total / count

if __name__ == '__main__':
    from inspect import getgeneratorstate
    avg_primer = average_primer()
    print(getgeneratorstate(avg_primer))  # GEN_SUSPENDED
    print(avg_primer.send(1))
    print(avg_primer.send(3))
    print(avg_primer.send(5))

在 coroutine 函数中把被装饰的生成器函数替换成这里的 primer 函数,接着调用被装饰的函数,获取生成器对象,预激生成器,最后返回预激后的生成器。根据 getgeneratorstate 得到的状态为 GEN_SUSPENDED 可知,此时 avg_primer 状态已经不是 GEN_CREATED 协程未启动状态,而是在yield表达式处暂停。


4. 终止协程和异常处理

协程中未处理的异常会向上冒泡,传给next函数或send方法的调用方(即触发协程的对象)。

客户代码可以在生成器对象上调用两个方法,显式地把异常发给协程。这两个方法是 throwclose

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

generator.close() 致使生成器在暂停的yield表达式处抛出GeneratorExit异常。如果生成器没有处理这个异常,或者抛出了StopIteration异常(通常是指运行到结尾),调用方不会报错。如果收到GeneratorExit异常,生成器一定不能产出值,否则解释器会抛出RuntimeError异常。生成器抛出的其他异常会向上冒泡,传给调用方。

from inspect import getgeneratorstate

class DemoException(BaseException):
    """为这次演示定义的异常类型。"""
    pass

def demo_exc_handling():
    print('-> coroutine started')
    while True:
        try:
            var = yield
        except DemoException as e:
            print('*** DemoException handled. Continuing...')
        else:
            print('-> coroutine received: {!r}'.format(var))
    # raise RuntimeError('This line should never run.')

if __name__ == '__main__':

    cor_exc = demo_exc_handling()
    cor_exc.send(None)
    print(getgeneratorstate(cor_exc))  # GEN_SUSPENDED
    cor_exc.send(1)               # -> coroutine received: 1   
    cor_exc.send(2)               # -> coroutine received: 2
    cor_exc.close()
    print(getgeneratorstate(cor_exc))  # GEN_CLOSED
# 如果把DemoException异常传入demo_exc_handling协程,它会处理,然后继续运行
cor_exc.throw(DemoException)  # *** DemoException handled. Continuing...
print(getgeneratorstate(cor_exc))  # GEN_SUSPENDED
# 如果传入协程的异常没有处理,协程会停止,即状态变成'GEN_CLOSED'
cor_exc.throw(ZeroDivisionError)
Traceback (most recent call last):
		...
    ZeroDivisionError
# 由于前面已经抛出异常了,所以后面的代码无法执行,只有在控制台操作才能看出状态
print(getgeneratorstate(cor_exc))  # GEN_CLOSED

如果不管协程如何结束都想做些清理工作,要把协程定义体中相关的代码放入try/finally块中,

def demo_exc_handling():
    print('-> coroutine started')
    try:
        while True:
            try:
                var = yield
            except DemoException as e:
                print('*** DemoException handled. Continuing...')
            else:
                print('-> coroutine received: {!r}'.format(var))
    finally:
        print('-> coroutine ending, clearing something.')

5. 让协程返回值
from collections import namedtuple

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

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

if __name__ == '__main__':
    avg = average()
    avg.send(None)
    avg.send(1)
    avg.send(2)
    avg.send(3)
    avg.send(None)
'''
Traceback (most recent call last):
   ...
StopIteration: Result(count=3, average=2.0)
'''

发送 None 会终止循环,导致协程结束,返回结果。一如既往,生成器对象会抛出StopIteration 异常。异常对象的 value 属性保存着返回的值。

何获取协程返回的值: 把 avg.send(None) 用try包含。

try:
    avg.send(None)
except StopIteration as e:
    print(e)
# Result(count=3, average=2.0)

6. 使用yield from

yield from 结构会在内部自动捕获 StopIteration 异常。这种处理方式与for 循环处理StopIteration 异常的方式一样:循环机制使用用户易于理解的方式处理异常。对 yield from 结构来说,解释器不仅会捕获 StopIteration 异常,还会把 value 属性的值变成 yield from 表达式的值。

yield from 可用于简化 for 循环中的 yield 表达式:

def gen():
    for i in "ABC":
        yield i
    for j in [1, 2, 3]:
        yield j

print(list(gen()))  # ['A', 'B', 'C', 1, 2, 3]

使用yield from:

def gen2():
    yield from "ABC"
    yield from [1, 2, 3]

print(list(gen2()))  # ['A', 'B', 'C', 1, 2, 3]

yield from 在扁平化处理嵌套型的序列中的应用:

def gen3(*args):
    for item in args:
        yield from item

a = (1, 2, 3)
b = "ABC"
print(list(gen3(a, b)))  # [1, 2, 3, 'A', 'B', 'C']

yield from x 表达式对 x 对象所做的第一件事是,调用 iter(x),从中获取迭代器,因此 x 可以是任何可迭代的对象。

如果 yield from 结构唯一的作用是替代产出值的嵌套 for 循环,这个结构很有可能不会添加到 Python 语言中。yield from 结构的本质作用无法通过简单的可迭代对象说明,而要发散思维,使用嵌套的生成器。

yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。有了这个结构,协程可以通过以前不可能的方式委托职责。(见《流畅的Python》)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值