Python 协程深入理解

从语法上来看,协程和生成器类似,都是定义体中包含yield关键字的函数。 yield在协程中的用法:

  • 在协程中yield通常出现在表达式的右边,例如:datum = yield,可以产出值,也可以不产出--如果yield关键字后面没有表达式,那么生成器产出None.
  • 协程可能从调用方接受数据,调用方是通过send(datum)的方式把数据提供给协程使用,而不是next(...)函数,通常调用方会把值推送给协程。
  • 协程可以把控制器让给中心调度程序,从而激活其他的协程

    所以总体上在协程中把yield看做是控制流程的方式

了解协程的过程

def simple_coroutine():
    print('coroutine start')
    x = yield
    print('coroutine received :', x)


my_coro = simple_coroutine()
next(my_coro)
my_coro.send(10)
运行结果
coroutine start
coroutine received : 10
Traceback (most recent call last):
  File "d:\Code\Python\day6\app.py", line 19, in <module>
    my_coro.send(10)
StopIteration

yield右边没有没有表达式,默认产出的值是None,刚开始调用next()方法,目的在于激活协程,程序就会运行到x = yield处,这里需要注意,这里程序运行到x = yield处,并没有将值赋值给x,而是计算yield 后面的值表达式,然后返回给next()方法,当这个生成器send()一个值给协程之后,从暂定处yieldsend的这个值赋值给x,然后继续运行,直到运行到下一个yield处。

当程序运行到最后,会自动抛出一个StopIteration的异常,当捕获异常之后,可以找到这个生成器最后的值

def simple_coroutine():
    print('coroutine start')
    x = yield
    print('coroutine received :', x)


try:
    my_coro = simple_coroutine()
    next(my_coro)
    my_coro.send(10)
except StopIteration as e:
    print('执行完毕之后的值:', e.value)
运行结果
coroutine start
coroutine received : 10
执行完毕之后的值: None

JavaScript Generator类似,看一个例子


def simple_coroutine(x, y):
    z = yield x + y
    x = yield z * x
    y = yield x + y + z
    return y


try:
    my_coro = simple_coroutine(5, 6)
    print(next(my_coro))
    print(my_coro.send(30))
    print(my_coro.send(8))
    print(my_coro.send('Done'))
except StopIteration as e:
    print('执行完毕之后的值:', e.value)
运行结果
11
150
44
执行完毕之后的值: (8, 'Done', 30)

当预激活传入x = 5,y =6时,第一次调用next()当遇到yield关键字,则交出函数的控制权,将yield后面的表达式计算出并返回给next(my_coro)中,所以当print(next(my_coro))的时候,值是x + y = 11

第二步send(30)即恢复函数的执行权,并将30赋值给第一次交出函数控制权的地方,即 z = yield x + y处,此时send的值为30,则将z = 30简单讲就是将 yield x + y 替换成 传入的值30,所以z = 30 继续执行,此时x = 5,y = 6 z= 30遇到yield关键字,交出函数的控制权,并计算yield后面的表达式返回,此时表达式为z * x,当前z = 30, x = 5,所以计算出值为150

第三步,send(8) 恢复函数执行权,并将8赋值给上一次交出函数控制权的地方,将 8 赋值给 x ,此时x = 8,y = 6, z= 30,继续运行程序,当遇到yield x + y +z继续交出函数控制权,返回x + y + z表达式的值44

第四步,send('Done') 继续恢复函数执行权,将Done 赋值给上一次交出函数控制权的地方,将Done 赋值给y ,此时x = 8, y = 'Done', z = 30,继续执行,知道执行到return处,整个控制流程结束,Python抛出StopIteration异常,捕获异常可以得到return的值(8, 'Done', 30)

运行过程

协程的运行过程中有4个状态

  • GEN_CREATE:等待开始执行
  • GEN_RUNNING:解释器正在执行,这个状态一般看不到
  • GEN_SUSPENDED:在yield表达式处暂停
  • GEN_CLOSED:执行结束

通过导入from inspect import getgeneratorstate来获取协程状态

from inspect import getgeneratorstate


def simple_coroutine(x, y):
    z = yield x + y
    x = yield z * x
    y = yield x + y + z
    return (x, y, z)


try:
    my_coro = simple_coroutine(5, 6)
    print(getgeneratorstate(my_coro))
    print(next(my_coro))
    print(getgeneratorstate(my_coro))
    print(my_coro.send(30))
    print(my_coro.send(8))
    print(my_coro.send('Done'))
except StopIteration as e:
    print(getgeneratorstate(my_coro))
    print('执行完毕之后的值:', e.value)
运行结果
GEN_CREATED
11
GEN_SUSPENDED
150
44
GEN_CLOSED
执行完毕之后的值: (8, 'Done', 30)

可以看到在未调用next()方法时,协程的状态为GUN_CREATED,在开始执行的时候协程的状态为GEN_SUSPENDED,最后执行完毕之后状态为GEN_CLOSED

预激协程的装饰器

from functools import wraps


def coroutine(func):
    @wraps(func)
    def prime(*args, **kwargs):
        gen = func(*args, **kwargs)
        print(next(gen))
        return gen
    return prime


@coroutine
def simple_coroutine(x, y):
    z = yield x + y
    x = yield z * x
    y = yield x + y + z
    return (x, y, z)


try:
    coro_arg = simple_coroutine(5, 6)
    print(coro_arg.send(30))
    print(coro_arg.send(8))
    coro_arg.send(None)
except StopIteration as e:
    print(e.value)

关于预激,在使用yield from句法调用协程的时候,会自动预激活,这样其实与我们上面定义的coroutine装饰器是不兼容的,在python3.4里面的asyncio.coroutine装饰器不会预激协程,因此兼容yield from

关于yield from

yield from 是在Python3.3才出现的语法。所以这个特性在Python2中是没有的

yield from 后面需要加的是可迭代对象,它可以是普通的可迭代对象,也可以是迭代器,甚至是生成器。

简单应用:拼接可迭代对象

使用yield和使用yield from的例子来对比

myStr = 'abc'
myList = [1, 2, 3]
mydict = {'name': 'aaron', 'age': '21'}
mygen = (i for i in range(4, 9))


def gen(*args):
    for item in args:
        for i in item:
            yield i


newList = gen(myStr, myList, mydict, mygen)
print(list(newList))
# ['a', 'b', 'c', 1, 2, 3, 'name', 'age', 4, 5, 6, 7, 8]
myStr = 'abc'
myList = [1, 2, 3]
mydict = {'name': 'aaron', 'age': '21'}
mygen = (i for i in range(4, 9))


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


newList = gen(myStr, myList, mydict, mygen)
print(list(newList))
# ['a', 'b', 'c', 1, 2, 3, 'name', 'age', 4, 5, 6, 7, 8]

由上面两种方式对比,可以看出,yield from后面加上可迭代对象,他可以把可迭代对象里的每个元素一个一个的yield出来,对比yield来说代码更加简洁,结构更加清晰

复杂应用:生成器的嵌套

yield from 后面加上一个生成器后,就实现了生成的嵌套。

当然实现生成器的嵌套,并不是一定必须要使用yield from,而是使用yield from可以让我们避免让我们自己处理各种料想不到的异常,而让我们专注于业务代码的实现。

  • 调用方:调用委派生成器的客户端(调用方)代码
  • 委托生成器: 包含yield from表达式的生成器函数
  • 子生成器: yield from后面加的生成器函数
# 委托生成器
def gen():
    while True:
        yield from averger_gen()

# 子生成器


def averger_gen():
    total = 0
    count = 0
    averger = 0
    while True:
        averger = yield averger
        total += averger
        count += 1
        averger = total / count


gen = gen()
next(gen)
print(gen.send(10))
print(gen.send(20))
print(gen.send(30))

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

调用方可以通过send()直接发送消息给子生成器,而子生成器yield的值,也是直接返回给调用方

委托生成器,只起一个桥梁作用,它建立的是一个双向通道,它并没有权利也没有办法,对子生成器yield回来的内容做拦截。

# 委托生成器
def gen():
    while True:
        # 只有子生成器要结束(return)了,yield from左边的变量才会被赋值,后面的代码才会执行。
        total, count, averger = yield from averger_gen()
        print('计算完成!总共计算:{}个,总和:{}分,平均分:{}'.format(count, total, averger))

# 子生成器


def averger_gen():
    total = 0
    count = 0
    averger = 0
    while True:
        term = yield averger
        if term is None:
            break
        total += term
        count += 1
        averger = total / count
    # 每一次return,都意味着当前协程结束。
    return total, count, averger


gen = gen()
next(gen)
print(gen.send(10))
print(gen.send(20))
print(gen.send(30))
gen.send(None)  # 结束协程
运行结果
10.0
15.0
20.0
计算完成!总共计算:3个,总和:60分,平均分:20.0
Copyright © aaron 2023 all right reserved,powered by Gitbook该文章修订时间: 2020-03-27 20:50:00

results matching ""

    No results matching ""