迭代器-->生成器-->协程的进化过程(python)

协程是一种比线程更轻量级的编程方式,

Python3.5 加入了关键字 async 和 await ,将生成器和协程彻底分开。

这个 PEP 把协程从生成器独立出来,成为 Python 的一个原生事物。这会消除协程和生成器之间的混淆,方便编写不依赖特定库的协程代码。

使用原生协程和相应的新语法,我们可以在异步编程时使用协程上下文管理器和迭代器。如下文所示,新的 async with 语句可以在进入、离开运行上下文时进行异步调用,而 async for 语句可以在迭代时进行异步调用。

I/O 多路复用是一个线程里会监视多个文件描述符,在其中一个或者多个描述符准备好之后,内核会通知用户进程,用户进程来处理数据。

        协程是一种比线程更轻量级的并发执行方式,它允许在函数执行过程中暂停和恢复执行,以便更好地管理程序的执行流程。协程主要用于异步编程,可以在单个线程内实现并发执行,从而提高程序的效率。

       在python中,协程是由生成器进化而来,而生成器又是由迭代器进化而来。我们先从迭代器开始讲解。迭代器是python中一种特殊的可迭代对象,可以用for循环对迭代器进行遍历,与其它可迭代对象如列表不一样的是,迭代器只能被遍历一次。这样的好处是每个元素都是迭代器一步步算出来的,而不是像列表这样一开始就在内存中。所有迭代器都实现了__iter__和__next__这两个特殊的方法。

下面是用python实现的一个求斐波那契数列的迭代器:

class Test:
    def __init__(self, n):
        self.a = 0
        self.b = 1
        self.n = n
    def __iter__(self):
        return self
    def __next__(self):
        self.a, self.b = self.b, self.a + self.b
        if self.a > self.n:
            raise StopIteration('超过了 n')
        return self.a

创建迭代器类时,__iter__ 和 __next__ 方法均只能有 self 一个参数,前者的返回值为实例本身,后者的返回值为迭代器取出的元素。从这个程序中可以更清楚地看出迭代器中的元素是计算出来的,而不是像列表那样存在内存中。

        生成器首先它是一个迭代器,和迭代器一样,生成器只能被遍历迭代一次,因为每次迭代的元素不是像列表元素一样,已经在内存中,而是每迭代一次生成一个元素。迭代器的优点生成器都有。生成器有两种创建方式,一种是用生成器表达式,一种是使用 Python 关键字 yield 编写的函数叫做生成器函数,函数的返回值就是生成器。

# 用生成器表达式创建生成器
g = (x**x for x in range(1, 4))


# 用yield表达式创建生成器
def fib(n):
    current = 0
    a = b = 1
    while current < n:
        yield a
        a, b = b, a + b
        current += 1

        函数体内部有 yield 关键字的都是生成器函数,fib 是生成器函数。yield 关键字只能出现在函数中,生成器函数的执行结果是生成器,注意这里所讲的 “执行结果” 不是函数的 return 值。生成器终止时必定抛出 StopIteration 异常,for 循环可以捕获此异常,异常的 value 属性值为生成器函数的 return 值。生成器还可以使用 next 方法迭代。生成器会在 yield 语句处暂停,这是至关重要的,未来协程中的 IO 阻塞就出现在这里。

        了解了迭代器和生成器,我们再来看生成器是如何进化为协程的。首先看一个示例, Python 内置模块 itertools 中有很多实用的方法,其中之一是 chain 方法,它可以接受任意数量的可迭代对象作为参数,返回一个包含所有参数中的元素的迭代器,下面我们分别用yield 关键字和yield from关键字来实现 chain 方法。

# 用yield关键字实现chain方法
def chain(*args):
    for iter_obj in args:
        for i in iter_obj:
            yield i

c = chain({'one', 'two'}, list('ace'))
for i in c:
    print(i)



# 用yield from 关键字实现chain方法
def chain(*args):
    for iter_obj in args:
        yield from iter_obj

c = chain({'one', 'two'}, list('ace'))
for i in c:
    print(i)

        可以看到 yield from 语句可以替代 for 循环,避免了嵌套循环。同 yield 一样,yield from 语句也只能出现在函数体内部,有 yield from 语句的函数叫做协程函数或生成器函数。yield from 后面接收一个可迭代对象,例如上面代码中的 iter_obj 变量,在协程中,可迭代对象往往是协程对象,这样就形成了嵌套协程。

        转移控制权是 yield from 语法的核心功能,也是从生成器进化到协程的最重要一步。下面举例说明转移控制权的功能。

# File Name: transfer_control.py

import time
from faker import Faker
from functools import wraps

# 预激协程装饰器
def coroutine(func):
    @wraps(func)
    def wrapper(*args, **kw):
        g = func(*args, **kw)
        next(g)
        return g
    return wrapper

# 子生成器函数,这个生成器是真正做事的生成器
def sub_coro():
    l = []                      # 创建空列表
    while True:                 # 无限循环
        value = yield           # 调用方使用 send 方法发生数据并赋值给 value 变量
        if value == 'CLOSE':    # 如果调用方发生的数据是 CLOSE ,终止循环
            break
        l.append(value)         # 向列表添加数据
    return sorted(l)            # 返回排序后的列表

# 使用预激协程装饰器
# 创建带有 yield from 语句的父生成器函数
@coroutine
def dele_coro():
    # while True 可以多次循环,每次循环会创建一个新的子生成器 sub_coro()
    # 这里 while 只循环一次,创建两次 sub_coro 生成器
    # 这是由调用方,也就是 main 函数决定的
    # 这里之所以使用 while 循环,是因为避免父生成器终止并触发 StopIteration 异常
    while True:
        # yield from 会自动预激子生成器 sub_coro()
        # 所以 sub_coro 在定义时不可以使用预激协程装饰器
        # yield from 将捕获子生成器终止时触发的 StopIteration 异常
        # 并将异常的 value 属性值赋值给等号前面的变量 l
        # 也就是 l 变量的值等于 sub_coro 函数的 return 值
        # yield from 还实现了一个重要功能
        # 就是父生成器的 send 方法将发送值给子生成器
        # 并赋值给子生成器中 yield 语句等号前面的变量 value
        l = yield from sub_coro()
        print('排序后的列表:', l)
        print('------------------')

# 调用父生成器的函数,也叫调用方
def main():
    # 生成随机国家代号的方法
    fake = Faker().country_code
    # 嵌套列表,每个子列表中有三个随机国家代号(字符串)
    nest_country_list = [[fake() for i in range(3)] for j in range(3)]
    for country_list in nest_country_list:
        print('国家代号列表:', country_list)
        c = dele_coro()      # 创建父生成器
        for country in country_list:
            c.send(country)  # 父生成器的 send 方法将国家代号发送给子生成器
        # CLOSE 将终止子生成器中的 while 循环
        # 子生成器的 return 值赋值给父生成器 yield from 语句中等号前面的变量 l
        c.send('CLOSE')

if __name__ == '__main__':
    main()

        在 Python 3.4 中,asyncio 模块出现,此时创建协程函数须使用 asyncio.coroutine 装饰器标记。此前的包含 yield from 语句的函数既可以称作生成器函数也可以称作协程函数,为了突出协程的重要性,现在使用 asyncio.coroutine 装饰器的函数就是真正的协程函数了。在 Python 3.5 中新增了 async / await 关键字用来定义协程函数。这两个关键字是一个组合,其作用等同于 asyncio.coroutine 装饰器和 yield from 语句。此后协程与生成器就彻底泾渭分明了。

在 asyncio 模块中出现了一些新的概念,我们来认识它们:

coroutine 协程

协程对象,使用 asyncio.coroutine 装饰器装饰的函数被称作协程函数,它的调用不会立即执行函数,而是返回一个协程对象,即协程函数的运行结果为协程对象,注意这里说的 “运行结果” 不是 return 值。协程对象需要包装成任务注入到事件循环,由事件循环调用。

task 任务

将协程对象作为参数创建任务,任务是对协程对象的进一步封装,其中包含任务的各种状态。

event_loop 事件循环

如果将多线程比喻为工厂里的多个车间,那么协程就是一个车间内的多台机器。在线程级程序中,一台机器开始工作,车间内的其它机器不能同时工作,需要等上一台机器停止,但其它车间内的机器可以同时启动,这样就可以显著提高工作效率。在协程程序中,一个车间内的不同机器可以同时运转,启动机器、暂停运转、延时启动、停止机器等操作都可以人为设置。

事件循环能够控制任务运行流程,也就是任务的调用方。

def four():
    start = time.time()
    async def corowork(name, t):
        print('[corowork] Start coroutine', name)
        await asyncio.sleep(t)                  # 1
        print('[corowork] Stop coroutine', name)
        return 'Coroutine {} OK'.format(name)   # 2

    loop = asyncio.get_event_loop()
    coroutine1 = corowork('ONE', 3)             # 3
    coroutine2 = corowork('TWO', 1)             # 3
    task1 = loop.create_task(coroutine1)        # 4
    task2 = loop.create_task(coroutine2)        # 4
    gather = asyncio.gather(task1, task2)       # 5
    loop.run_until_complete(gather)             # 6
    print('[task1] ', task1.result())           # 7
    print('[task2] ', task2.result())           # 7

    end = time.time()
    print('运行耗时:{:.4f}'.format(end - start))

代码说明:

1、await 关键字等同于 Python 3.4 中的 yield from 语句,后面接协程对象。asyncio.sleep 方法的返回值为协程对象,这一步为阻塞运行。asyncio.sleep 与 time.sleep 是不同的,前者阻塞当前协程,即 corowork 函数的运行,而 time.sleep 会阻塞整个线程,所以这里必须用前者,阻塞当前协程,CPU 可以在线程内的其它协程中执行

2、协程函数的 return 值可以在协程运行结束后保存到对应的 task 对象的 result 方法中

3、创建两个协程对象,在协程内部分别阻塞 3 秒和 1 秒

4、创建两个任务对象

5、将任务对象作为参数,asyncio.gather 方法创建任务收集器。注意,asyncio.gather 方法中参数的顺序决定了协程的启动顺序

6、将任务收集器作为参数传入事件循环的 run_until_complete 方法,阻塞运行,直到全部任务完成

7、任务结束后,事件循环停止,打印任务的 result 方法返回值,即协程函数的 return 值

到这一步,大家应该可以看得出,上面的代码已经是异步编程的结构了,在事件循环内部,两个协程是交替运行完成的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值