【13.0】协程操作

  • 基于 async 和 await 关键字的协程可以实现异步编程,这也是目前 Python 异步相关的主流技术。在这里我们主要介绍一下实现异步的模块:asyncio 模块

【一】asyncio 模块

  • asyncio 模块是 Python 中实现异步的一个模块,该模块在 Python3.4 的时候发布
  • async 和 await 关键字在 Python3.5 中引入。
  • 因此,想要使用asyncio模块,建议 Python 解释器的版本不要低于 Python3.5 。

【二】事件循环

  • 所谓的事件循环,我们可以把它当作是一个 while 循环,这个 while 循环在循环生命周期内运行并执行一些任务,在特定的条件下结束循环。
  • 在编写程序的时候可以通过如下代码来获取和创建事件循环:
import asyncio

loop = asyncio.get_event_loop()

【三】协程函数和协程对象

【1】什么是协程函数

  • 首先我们来看一下协程函数
    • 什么是协程函数呢?
  • 直白的讲,定义为如下形式的函数
  • 我们可以称之为协程函数,如下代码所示:
# 使用 async 声明的函数就是协程函数
async def fn():
    pass

【2】什么是协程对象

  • 知道了什么是协程函数
    • 接下来我们再来看一下什么是协程对象
    • 所谓的协程对象就是调用协程函数之后返回的对象
    • 我们称之为 协程对象,如下代码所示:
# 使用 async 声明的函数就是协程函数
async def fn():
    pass


# 调用携程函数得到的对象就是协程对象
res = fn()
print(res) # <coroutine object fn at 0x1029684a0>

  • 注意事项:调用协程函数时,函数内部的代码不会执行,只是会返回一个协程对象!

【四】协程函数应用

【1】基本应用

  • 在编写程序的时候,如果想要执行协程函数内部的代码,通过 函数名() 调用函数是不可以的,需要 事件循环协程对象 配合才能实现,如下代码所示:
import asyncio


async def fn():
    print('协程函数内部的代码')


# 执行协程代码的方式一
def main_first():
    # 调用协程函数,返回一个协程对象
    res = fn()

    # todo:1、创建一个事件循环
    loop = asyncio.get_event_loop()

    # todo:2、将协程当作任务提交到事件循环的任务列表中,协程执行完成之后终止
    loop.run_until_complete(res)


# 执行协程代码的方式二
def main_second():
    # 调用协程函数,返回一个协程对象
    res = fn()
    # 解析:第二种方式在本质上和第一种方式是相同的,其内部先创建事件循环,然后执行 run_until_complete
    # 但是要注意:该方式只支持 Python3.7+ 的解释器,因为该方式在 Python3.7 加入的。
    asyncio.run(res)


if __name__ == '__main__':
    main_first()

    main_second()
  • 这个过程可以简单理解为:
    • 协程函数 当做任务添加到 事件循环 的任务列表
    • 然后事件循环检测列表中的协程函数 是否已准备就绪(默认可理解为就绪状态)
    • 如果准备就绪则执行其内部代码。

【2】await关键字

  • await 是一个 只能 在协程函数中使用的关键字,用于当协程函数遇到IO操作的时候挂起当前协程(任务),
  • 当前协程挂起过程中,事件循环可以去执行其他的协程(任务)
  • 当前协程IO处理完成时,可以再次切换回来执行 await 之后的代码
  • 怎么理解呢?请看如下代码:

(1)实例1

import asyncio


async def fn():
    print('协程函数内部的代码')

    # 遇到IO操作之后挂起当前协程(任务),等IO操作完成之后再继续往下执行。
    # 当前协程挂起时,事件循环可以去执行其他协程(任务)
    response = await asyncio.sleep(2)  # 模拟遇到了IO操作

    print(f'IO请求结束,结果为:{response}')


def main():
    # 调用协程函数,返回一个协程对象
    res = fn()

    # 执行协程函数
    asyncio.run(res)


if __name__ == '__main__':
    main()

    '''
    运行结果:
    协程函数内部的代码
    IO请求结束,结果为:None
    '''

(2)实例2

import asyncio


async def other_tasks():
    print('start')
    await asyncio.sleep(2)  # 模拟遇到了IO操作
    print('end')
    return '返回值'


async def fn():
    print('协程函数内部的代码')

    # 遇到IO操作之后挂起当前协程(任务),等IO操作完成之后再继续往下执行。
    # 当前协程挂起时,事件循环可以去执行其他协程(任务)
    response = await other_tasks()  # 模拟执行其他协程任务

    print(f'IO请求结束,结果为:{response}')


def main():
    # 调用协程函数,返回一个协程对象
    res = fn()

    # 执行协程函数
    asyncio.run(res)


if __name__ == '__main__':
    main()

    '''
    运行结果:
    协程函数内部的代码
    start
    end
    IO请求结束,结果为:返回值
    '''

(3)实例3

import asyncio


async def other_tasks():
    print('start')
    await asyncio.sleep(2)  # 模拟遇到了IO操作
    print('end')
    return '返回值'


async def fn():
    print('协程函数内部的代码')

    # 遇到IO操作之后挂起当前协程(任务),等IO操作完成之后再继续往下执行。
    # 当前协程挂起时,事件循环可以去执行其他协程(任务)
    respnse1 = await other_tasks()
    print(f'IO请求结束,结果为:{respnse1}')

    respnse2 = await other_tasks()
    print(f'IO请求结束,结果为:{respnse2}')


def main():
    # 调用协程函数,返回一个协程对象
    cor_obj = fn()

    # 执行协程函数
    asyncio.run(cor_obj)


if __name__ == '__main__':
    main()
    
    '''
    运行结果:
    协程函数内部的代码
    start
    end
    IO请求结束,结果为:返回值
    start
    end
    IO请求结束,结果为:返回值
    '''

(4)小结

  • 上述的所有实例都只是创建了一个任务
    • 即:事件循环的任务列表中只有一个任务
    • 所以在IO等待时无法演示切换到其他任务效果。
  • 在程序中想要创建多个任务对象
    • 需要使用Task对象来实现。

【3】Task 对象

  • Tasks 用于并发调度协程
  • 通过 asyncio.create_task(协程对象) 的方式创建 Task 对象
  • 这样可以让协程加入事件循环中等待被调度执行。
  • 除了使用 asyncio.create_task() 函数以外
  • 还可以用低层级的loop.create_task() 或 ensure_future() 函数。并且不建议手动实例化 Task 对象。
  • 本质上是将协程对象封装成 Task 对象
  • 并将协程立即加入事件循环,同时追踪协程的状态。
  • 注意事项:
    • asyncio.create_task() 函数在 Python3.7 中被加入。
    • 在 Python3.7 之前,可以改用低层级的
    • asyncio.ensure_future() 函数。

(1)协程运行方式一

  • async.run() 运行协程
  • async.create_task()创建task
import asyncio


async def other_tasks():
    print('start')
    await asyncio.sleep(2)  # 模拟遇到了IO操作
    print('end')
    return '返回值'


async def fn():
    print('fn开始')

    # 创建协程,将协程封装到一个Task对象中并立即添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
    task1 = asyncio.create_task(other_tasks())

    # 创建协程,将协程封装到一个Task对象中并立即添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
    task2 = asyncio.create_task(other_tasks())

    print('fn结束')

    # 当执行某协程遇到IO操作时,会自动化切换执行其他任务。
    # 此处的await是等待相对应的协程全都执行完毕并获取结果
    response1 = await task1
    response2 = await task2
    print(response1, response2)


def main():
    asyncio.run(fn())


if __name__ == '__main__':
    main()

    '''
    运行结果:
    fn开始
    fn结束
    start
    start
    end
    end
    返回值 返回值
    '''

(2)协程运行方式二

import asyncio


async def other_tasks():
    print('start')
    await asyncio.sleep(2)  # 模拟遇到了IO操作
    print('end')
    return '返回值'


async def fn():
    print('fn开始')

    # 创建协程,将协程封装到一个Task对象中并立即添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
    task_lis = [
        asyncio.create_task(other_tasks()),
        asyncio.create_task(other_tasks()),
    ]

    print('fn结束')
    # 当执行某协程遇到IO操作时,会自动化切换执行其他任务。
    # 此处的await是等待所有协程执行完毕,并将所有协程的返回值保存到done
    # 如果设置了timeout值,则意味着此处最多等待的秒,完成的协程返回值写入到done中,未完成则写到pending中。
    done, pending = await asyncio.wait(task_lis, timeout=None)

    print(f"done :>>>> {done}")
    print(f"pending :>>>> {pending}")


def main():
    asyncio.run(fn())


if __name__ == '__main__':
    main()

    '''
    fn开始
    fn结束
    start
    start
    end
    end
    done :>>>> {<Task finished name='Task-2' coro=<other_tasks() done, defined at /Users/dream/Desktop/PythonProjects/12并发编程/02协程/01.py:4> result='返回值'>, <Task finished name='Task-3' coro=<other_tasks() done, defined at /Users/dream/Desktop/PythonProjects/12并发编程/02协程/01.py:4> result='返回值'>}
    pending :>>>> set()
    '''

(3)获取协程返回值

  • async.gather()获取返回值
import asyncio


async def other_tasks():
    print('start')
    await asyncio.sleep(2)  # 模拟遇到了IO操作
    print('end')
    return '返回值'


async def fn():
    print('fn开始')

    # 创建协程,将协程封装到一个Task对象中并立即添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
    task_lis = [
        asyncio.create_task(other_tasks()),
        asyncio.create_task(other_tasks()),
    ]

    print('fn结束')
    # 当执行某协程遇到IO操作时,会自动化切换执行其他任务。
    # 此处的await是等待所有协程执行完毕,并将所有协程的返回值保存到done
    # 如果设置了timeout值,则意味着此处最多等待的秒,完成的协程返回值写入到done中,未完成则写到pending中。
    await asyncio.wait(task_lis, timeout=None)

    response = await asyncio.gather(task_lis[0], task_lis[1])  # 将task_lis作为参数传入gather,等异步任务都结束后返回结果列表

    print(f'response :>>>> {response}')


def main():
    asyncio.run(fn())


if __name__ == '__main__':
    main()

    '''
    fn开始
    fn结束
    start
    start
    end
    end
    response :>>>> ['返回值', '返回值']
    '''

【4】aiohtpp对象

  • 我们之前学习过爬虫最重要的模块requests,但它是阻塞式的发起请求,每次请求发起后需阻塞等待其返回响应,不能做其他的事情。
    • 本文要介绍的aiohttp可以理解成是和requests对应Python异步网络请求库,它是基于 asyncio 的异步模块,可用于实现异步爬虫,有点就是更快于 requests 的同步爬虫。
    • 安装方式,pip install aiohttp。
  • aiohttp是一个为Python提供异步HTTP 客户端/服务端编程,基于asyncio的异步库。
    • asyncio可以实现单线程并发IO操作,其实现了TCP、UDP、SSL等协议,
    • aiohttp就是基于asyncio实现的http框架。
import aiohttp
import asyncio

async def main():
    async with aiohttp.ClientSession() as session:
        async with session.get("http://httpbin.org/headers") as response:
            print(await response.text())

asyncio.run(main())

【五】异步迭代器

【1】什么是异步迭代器?

  • 实现了 aiter() 和 anext()方法的对象。
  • anext 必须返回一个 awaitable 对象。
  • async for会处理异步迭代器的 anext()方法所返回的可等待对象,直到其引发一个 StopAsyncIteration异常。

【2】什么是异步可迭代对象?

  • 可在 async for语句中被使用的对象。
  • 必须通过它的 aiter()方法返回一个 asynchronous iterator 。
import asyncio


class Reader:
    """ 自定义异步迭代器(同时也是异步可迭代对象) """

    def __init__(self):
        self.count = 0

    async def readline(self):
        self.count += 1
        if self.count == 100:
            return None
        return self.count

    def __aiter__(self):
        return self

    async def __anext__(self):
        val = await self.readline()
        if val is None:
            raise StopAsyncIteration
        return val


async def fn():
    # 创建异步可迭代对象

    async_iter = Reader()
    # async for 必须放在async def 函数内,否则语法错误。
    async for item in async_iter:
        print(item)


asyncio.run((fn()))

【六】异步上下文管理器

  • 此种对象通过定义 __aenter__()__aexit__() 方法来对 async with 语句中的环境进行控制。
import asyncio


class AsyncContextManager:

    def __init__(self):
        self.conn = None

    async def do_something(self):
        # 异步操作数据库
        return 123

    async def __aenter__(self):
        # 异步链接数据库
        self.conn = await asyncio.sleep(1)
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        # 异步关闭数据库链接
        await asyncio.sleep(1)


async def fn():
    async with AsyncContextManager() as f:
        res = await f.do_something()
        print(res)


asyncio.run(fn())

【七】小结

  • 在程序中只要看到 asyncawait 关键字
  • 其内部就是基于协程实现的异步编程
  • 这种异步编程是通过一个线程在IO等待时间去执行其他任务,从而实现并发。

以上就是异步编程的常见操作,更多内容请参考 Python 官方文档:https://docs.python.org/zh-cn/3.8/library/asyncio.html

【八】小试牛刀

  • 需求:访问苹果官网 50 次,测试其总访问时间。

【1】同步版本

import time
import requests


def calc_visit_time(url):
    """
    计算程序的执行时间**
    :param url:
    :return:
    """
    start_time = time.time()
    for times in range(1, 51):
        response = requests.get(url=url)
        if response.status_code == 200:
            print(f'第【{times}】次链接成功!')
    else:
        print(f'链接失败!'.center(50, '-'))

    end_time = time.time()
    print(f'任务消耗了{end_time - start_time}秒的时间!')


def main():
    calc_visit_time('https://www.baidu.com')


if __name__ == '__main__':
    main()

    '''
    运行结果:
    第【1】次链接成功!
    第【2】次链接成功!
    第【3】次链接成功!
    第【4】次链接成功!
    第【5】次链接成功!
    ...
    第【46】次链接成功!
    第【47】次链接成功!
    第【48】次链接成功!
    第【49】次链接成功!
    第【50】次链接成功!
    任务消耗了5.65秒的时间!
    '''

【2】异步版本

# 异步版本

import asyncio
import time
import aiohttp


async def fetch_async(url):
    """
    异步的aiohttp模块,替代了之前的同步的requests模块
    :param url:
    :return:
    """
    async with aiohttp.ClientSession() as session:
        # 跳过证书验证 :>>>> ssl=False
        async with session.get(url=url, ssl=False) as response:
            if response.status == 200:
                print(f'链接成功!')
            else:
                print(f'链接失败!'.center(50, '-'))
        return True


async def calc_visit_time(url):
    """
    计算程序的执行时间
    :param url:
    :return:
    """

    start_time = time.time()
    task_lis = []
    for _ in range(50):
        task_lis.append(fetch_async(url))
    await asyncio.gather(*task_lis)
    end_time = time.time()
    print(f'任务消耗了{end_time - start_time}秒的时间!')


def main():
    asyncio.run(calc_visit_time('https://www.baidu.com'))


if __name__ == '__main__':
    main()

    '''
    运行结果:
    链接成功!
    链接成功!
    链接成功!
    链接成功!
    链接成功!
    ...
    链接成功!
    链接成功!
    链接成功!
    链接成功!
    链接成功!
    任务消耗了0.31秒的时间!
    '''
  • 通过上述两个实例的运行结果,我们可以看到,使用了异步之后,其效率提升18倍。
posted @ 2024-01-23 14:29  Chimengmeng  阅读(9)  评论(0编辑  收藏  举报
/* */