【Python基础】协程

一个协程的简单演示

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

my_coro = simple_coroutine()
print(my_coro)  # <generator object simple_coroutine at 0x000002A539D8D830>
next(my_coro)  # -> coroutine started   ,首先要先调用next()函数激活协程
my_coro.send(42)  # -> coroutine received: 42    ,StopIteration

产出两个值的协程

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

my_coro2 = simple_coro2(14)
print(getgeneratorstate(my_coro2))  # GEN_CREATED  ,协程未启动状态
print(next(my_coro2))  # -> Started: a = 14  14,向前执行协程到第一个yield表达式,打印出此消息,然后产出a的值并暂停,等待为b赋值
print(getgeneratorstate(my_coro2))  # GEN_SUSPENDED  ,协程在yield表达式暂停 状态
print(my_coro2.send(28))  # -> Received: b = 28  42  ,将28发给暂停的协程,计算yield表达式,得到28,然后将其绑定给b。打印此消息,产出a + b的值(42),然后暂停协程,等待为c赋值
my_coro2.send(99)  # -> Received: c = 99    ,StopIteration
print(getgeneratorstate(my_coro2))  # GEN_CLOSED  ,协程执行结束状态

使用协程计算移动平均值

totalcount声明为局部变量即可,无需使用实例属性或闭包在多次调用之间保持上下文

def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average  # yield表达式用于暂停执行协程,把结果发给调用方;还用于接收调用方后米娜发给协程的值,恢复无限循环
        total += term
        count += 1
        average = total/count

coro_avg = averager()
# coro_avg.send(None)  # 向协程发送None也可以激活协程,效果和下列代码一样
next(coro_avg)  # 调用next()激活协程,协程向前执行到yield表达式,产出变量值的初始值--None
print(coro_avg.send(10))  # 10.0
print(coro_avg.send(30))  # 20.0
print(coro_avg.send(5))  # 15.0

预激协程的装饰器

为了简化协程的用法,会使用一个预激装饰器(使用yield from句法调用协程时,会自动预激,与下面的@coroutine之类的装饰器不兼容)

from functools import wraps

def coroutine(func):
    """装饰器:向前执行到第一个'yield'表达式,预激'func'"""
    @wraps(func)  # wraps装饰器保证原函数的属性不变
    def primer(*args, **kwargs):
        gen = func(*args, **kwargs)
        next(gen)
        return gen
    return primer

@coroutine
def average():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average
        total += term
        count += 1
        average = total/count

coro_avg = average()  # 在coroutine装饰器的primer函数中已经预激了这个生成器
print(getgeneratorstate(coro_avg))  # GEN_SUSPENDED   ,表明该协程已经准备好了,可以接收值了
print(coro_avg.send(10))  # 10.0
print(coro_avg.send(30))  # 20.0
print(coro_avg.send(5))  # 15.0

终止协程和异常处理

未处理的异常会导致协程终止:

print(coro_avg.send('spam'))  # TypeError: unsupported operand type(s) for +=: 'float' and 'str'
try:
    coro_avg.send('spam')
except TypeError:
    pass
print(coro_avg.send(5))  # StopIteration  ,试图重新激活协程,抛出此异常

在协程中处理异常:

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

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))
    raise RuntimeError('This line should never run.')  # 这一行不会被执行到

正常激活和关闭demo_exc_handling协程:

exc_coro = demo_exc_handling()
next(exc_coro)  # -> coroutine started
exc_coro.send(11)  # -> coroutine received:11
exc_coro.send(22)  # -> coroutine received:22
exc_coro.close()
print(getgeneratorstate(exc_coro))  # GEN_CLOSED

将DemoException异常传入demo_exc_handling协程,它会处理后继续运行:

exc_coro = demo_exc_handling()
next(exc_coro)  # -> coroutine started
exc_coro.send(11)  # -> coroutine received:11
exc_coro.throw(DemoException)  # *** DemoException handled. Continuing...
print(getgeneratorstate(exc_coro))  # GEN_SUSPENDED

传入协程的异常如果没有处理,协程会停止:

exc_coro = demo_exc_handling()
next(exc_coro)  # -> coroutine started
exc_coro.send(11)  # -> coroutine received:11
# exc_coro.throw(ZeroDivisionError)  # ZeroDivisionError
try:
    exc_coro.throw(ZeroDivisionError)
except ZeroDivisionError:
    pass
print(getgeneratorstate(exc_coro))  # GEN_CLOSED

使用try/finally块在协程终止时执行操作:

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

让协程返回值:

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:  # 发送None会终止循环,导致协程结束,返回结果。一如既往,生成器对象会抛出StopIteration异常。异常对象的value属性保存着返回的值。
            break  # 为了返回值,协程必须正常终止;添加条件判断,以便退出累计循环
        total += term
        count += 1
        average = total/count
    return Result(count, average)

coro_avg = averager()
next(coro_avg)
coro_avg.send(10)
coro_avg.send(30)
coro_avg.send(6.5)
# coro_avg.send(None)  # StopIteration: Result(count=3, average=15.5)
try:
    coro_avg.send(None)
except StopIteration as exc:  # 捕获异常,获取averager返回的值
    result = exc.value
print(result)  # Result(count=3, average=15.5)

yield from的使用

简化for循环中的yield表达式:

def gen():
    for c in 'AB':
        yield c
    for i in range(1, 3):
        yield i
print(list(gen()))  # ['A', 'B', 1, 2]
# 使用yield from改写
def gen():
    yield from 'AB'
    yield from range(1, 3)
print(list(gen()))  # ['A', 'B', 1, 2]

使用yield from链接可迭代的对象:

def chain(*iterables):
    for it in iterables:
        yield from it
s = 'ABC'
t = tuple(range(3))
print(list(chain(s, t)))  # ['A', 'B', 'C', 0, 1, 2]

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

委派生成器和子生成器

一个使用委派生成器和子生成器的示例:使用 yield from 计算平均值并输出统计报告

from collections import namedtuple

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

子生成器:

def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield  # main函数中客户代码发送的各个值绑定到这里的term变量上
        if term is None:  # 终止条件,确实会导致 使用yield from调用这个协程的生成器会永远阻塞
            break
        total += term
        count += 1
        average = total/count
    return Result(count, average)  # 返回的Result会成为grouper函数中yield from表达式的值

委派生成器:

def grouper(results, key):
    while True:  # 每次迭代时会新建一个averager实例;每个实例都是作为协程使用的生成器对象
        results[key] = yield from averager()

客户端代码,即调用方:

def main(data):
    results = {}
    for key, values in data.items():
        group = grouper(results, key)
        next(group)
        # group.send(None)  ,这行代码效果等效上面的代码,都是预激协程
        for value in values:
            group.send(value)
        group.send(None)

    print(results)  # 用于调试
    '''{'girls;kg': Result(count=10, average=42.040000000000006), 
        'girls;m': Result(count=10, average=1.4279999999999997), 
        'boys;kg': Result(count=9, average=40.422222222222224), 
        'boys;m': Result(count=9, average=1.3888888888888888)}'''
    report(results)

输出报告:

def report(results):
    for key, result in sorted(results.items()):
        group, unit = key.split(';')
        print('{:2} {:5} averaging {:.2f}{}'.format(
            result.count, group, result.average, unit))

data = {
    '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.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
    'boys;kg':
        [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
    'boys;m':
        [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46],
}

if __name__ == '__main__':
    main(data)
'''
 9 boys  averaging 40.42kg
 9 boys  averaging 1.39m
10 girls averaging 42.04kg
10 girls averaging 1.43m
'''
  • 子生成器产出的值都直接传给委派生成器的调用方(即客户端代码);
  • 使用 send() 方法发给委派生成器的值都直接传给子生成器。如果发送的值是 None,那么会调用子生成器的 __next__() 方法。如果发送的值不是 None,那么会调用子生成器的 send() 方法。如果调用的方法抛出 StopIteration 异常,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器;
  • 生成器退出时,生成器(或子生成器)中的return expr表达式会触发 StopIteration(expr) 异常抛出;
  • yield from 表达式的值是子生成器终止时传给StopIteration异常的第一个参数。
yield from 结构的另外两个特性
  • 传入委派生成器的异常,除了GeneratorExit之外都传给子生成器的throw()方法。如果调用throw()方法时抛出StopIteration异常,委派生成器恢复运行。StopIteration之外的异常会向上冒泡,传给委派生成器。
  • 如果把GeneratorExit异常传入委派生成器,或者在委派生成器上调用close()方法,那么在子生成器上调用close()方法,如果它有的话。如果调用close()方法导致异常抛出,那么异常会向上冒泡,传给委派生成器;否则,委派生成器抛出GeneratorExit异常。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值