为什么你还不懂得怎么使用Python协程


Posted in Python onMay 13, 2019

前言

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

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

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

在前一篇《一文彻底搞懂Python可迭代(Iterable)、迭代器(Iterator)和生成器(Generator)的概念》 的文中,知道生成器(Generator)可由以下两种方式定义:

  • 列表生成器
  • 使用yield定义的函数

在Python早期的版本中协程也是通过生成器来实现的,也就是基于生成器的协程(Generator-based Coroutines)。在前一篇介绍生成器的文章末尾举了一个生产者-消费者的例子,就是基于生成器的协程来实现的。

def producer(c):
 n = 0
 while n < 5:
 n += 1
 print('producer {}'.format(n))
 r = c.send(n)
 print('consumer return {}'.format(r))


def consumer():
 r = ''
 while True:
 n = yield r
 if not n:
 return
 print('consumer {} '.format(n))
 r = 'ok'


if __name__ == '__main__':
 c = consumer()
 next(c) # 启动consumer
 producer(c)

看了这段代码,相信很多初学者和我一样对基于生成器的协程实现其实很难马上就能够根据业务写出自己的协程代码。Python实现者们也注意到这个问题,因为它太不Pythonic了。而基于生成器的协程也将被废弃,因此本文将重点介绍asyncio包的使用,以及涉及到的一些相关类概念。

注:我使用的Python环境是3.7。

0x00 何为协程(Coroutine)

协程(Coroutine)是在线程中执行的,可理解为微线程,但协程的切换没有上下文的消耗,它比线程更加轻量些。一个协程可以随时中断自己让另一个协程开始执行,也可以从中断处恢复并继续执行,它们之间的调度是由程序员来控制的(可以看本文开篇处生产者-消费者的代码)。

定义一个协程

在Python3.5+版本新增了aysnc和await关键字,这两个语法糖让我们非常方便地定义和使用协程。

在函数定义时用async声明就定义了一个协程。

import asyncio

# 定义了一个简单的协程
async def simple_async():
 print('hello')
 await asyncio.sleep(1) # 休眠1秒
 print('python')
 
# 使用asynio中run方法运行一个协程
asyncio.run(simple_async())

# 执行结果为
# hello
# python

在协程中如果要调用另一个协程就使用await。要注意await关键字要在async定义的函数中使用,而反过来async函数可以不出现await

# 定义了一个简单的协程
async def simple_async():
 print('hello')
 
asyncio.run(simple_async())

# 执行结果
# hello

asyncio.run()将运行传入的协程,负责管理asyncio事件循环。

除了run()方法可直接执行协程外,还可以使用事件循环loop

async def do_something(index):
 print(f'start {time.strftime("%X")}', index)
 await asyncio.sleep(1)
 print(f'finished at {time.strftime("%X")}', index)


def test_do_something():
 # 生成器产生多个协程对象
 task = [do_something(i) for i in range(5)]

 # 获取一个事件循环对象
 loop = asyncio.get_event_loop()
 # 在事件循环中执行task列表
 loop.run_until_complete(asyncio.wait(task))
 loop.close()

test_do_something()

# 运行结果
# start 00:04:03 3
# start 00:04:03 4
# start 00:04:03 1
# start 00:04:03 2
# start 00:04:03 0
# finished at 00:04:04 3
# finished at 00:04:04 4
# finished at 00:04:04 1
# finished at 00:04:04 2
# finished at 00:04:04 0

可以看出几乎同时启动了所有的协程。

其实翻阅源码可知asyncio.run()的实现也是封装了loop对象及其调用。而asyncio.run()每次都会创建一个新的事件循环对象用于执行协程。

0x01 Awaitable对象

在Python中可等待(Awaitable)对象有:协程(corountine)、任务(Task)、Future。即这些对象可以使用await关键字进行调用

await awaitable_object

1. 协程(Coroutine)

协程由async def声明定义,一个协程可由另一个协程使用await进行调用

async def nested():
 print('in nested func')
 return 13


async def outer():

 # 要使用await 关键字 才会执行一个协程函数返回的协程对象
 print(await nested())

asyncio.run(outer())

# 执行结果
# in nested func
# 13

如果在outer()方法中直接调用nested()而不使用await,将抛出一个RuntimeWarning

async def outer():
 # 直接调用协程函数不会发生执行,只是返回一个 coroutine 对象
 nested()
 
asyncio.run(outer())

运行程序,控制台将输出以下信息

RuntimeWarning: coroutine 'nested' was never awaited
  nested()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback

2. 任务(Task)

任务(Task)是可以用来并发地执行协程。可以使用asyncio.create_task()将一个协程对象封装成任务,该任务将很快被排入调度队列并执行。

async def nested():
 print('in nested func')
 return 13

async def create_task():
 # create_task 将一个协程对象打包成一个 任务时,该协程就会被自动调度运行
 task = asyncio.create_task(nested())
 # 如果要看到task的执行结果
 # 可以使用await等待协程执行完成,并返回结果
 ret = await task
 print(f'nested return {ret}')

asyncio.run(create_task())

# 运行结果
# in nested func
# nested return 13

注:关于并发下文还会详细说明。

3. Future

Future是一种特殊的低层级(low-level)对象,它是异步操作的最终结果(eventual result)。
当一个 Future 对象 被等待,这意味着协程将保持等待直到该 Future 对象在其他地方操作完毕。

通常在应用层代码不会直接创建Future对象。在某些库和asyncio模块中的会使用到该对象。

async def used_future_func():
 await function_that_returns_a_future_object()

0x02 并发

1. Task

前面我们知道Task可以并发地执行。  asyncio.create_task()就是一个把协程封装成Task的方法。

async def do_after(what, delay):
 await asyncio.sleep(delay)
 print(what)

# 利用asyncio.create_task创建并行任务
async def corun():
 task1 = asyncio.create_task(do_after('hello', 1)) # 模拟执行1秒的任务
 task2 = asyncio.create_task(do_after('python', 2)) # 模拟执行2秒的任务

 print(f'started at {time.strftime("%X")}')
 # 等待两个任务都完成,两个任务是并行的,所以总时间两个任务中最大的执行时间
 await task1
 await task2

 print(f'finished at {time.strftime("%X")}')

asyncio.run(corun())

# 运行结果
# started at 23:41:08
# hello
# python
# finished at 23:41:10

task1是一个执行1秒的任务,task2是一个执行2秒的任务,两个任务并发的执行,总共消耗2秒。

2. gather

除了使用asyncio.create_task()外还可以使用asyncio.gather(),这个方法接收协程参数列表

async def do_after(what, delay):
 await asyncio.sleep(delay)
 print(what)
 
async def gather():
 print(f'started at {time.strftime("%X")}')
 # 使用gather可将多个协程传入
 await asyncio.gather(
 do_after('hello', 1),
 do_after('python', 2),
 )
 print(f'finished at {time.strftime("%X")}')

asyncio.run(gather())

# 运行结果
# started at 23:47:50
# hello
# python
# finished at 23:47:52

两个任务消耗的时间为其中消耗时间最长的任务。

0x03 引用

docs.python.org/3/library/a…

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。

Python 相关文章推荐
python应用程序在windows下不出现cmd窗口的办法
May 29 Python
python删除过期文件的方法
May 29 Python
python itchat实现微信好友头像拼接图的示例代码
Aug 14 Python
Python如何实现MySQL实例初始化详解
Nov 06 Python
Python基于分析Ajax请求实现抓取今日头条街拍图集功能示例
Jul 19 Python
对Python3之方法的覆盖与super函数详解
Jun 26 Python
Python OpenCV实现鼠标画框效果
Aug 19 Python
pytorch实现seq2seq时对loss进行mask的方式
Feb 18 Python
详解PyQt5信号与槽的几种高级玩法
Mar 24 Python
Virtualenv 搭建 Py项目运行环境的教程详解
Jun 22 Python
一个非常简单好用的Python图形界面库(PysimpleGUI)
Dec 28 Python
Python 的 f-string 可以连接字符串与数字的原因解析
Feb 20 Python
Python玩转加密的技巧【推荐】
May 13 #Python
11个Python3字典内置方法大全与示例汇总
May 13 #Python
python中的数据结构比较
May 13 #Python
Python中函数的基本定义与调用及内置函数详解
May 13 #Python
python实现弹跳小球
May 13 #Python
Python开发之Nginx+uWSGI+virtualenv多项目部署教程
May 13 #Python
PyQt5的PyQtGraph实践系列3之实时数据更新绘制图形
May 13 #Python
You might like
中国站长站 For Dede4.0 采集规则
2007/05/27 PHP
php截取utf-8中文字符串乱码的解决方法
2010/03/29 PHP
Windows下XDebug 手工配置与使用说明
2010/07/11 PHP
Javascript的匿名函数小结
2009/12/31 Javascript
jQuery UI 应用不同Theme的办法
2010/09/12 Javascript
jquery使用$(element).is()来判断获取的tagName
2014/08/24 Javascript
20个实用的JavaScript技巧分享
2014/11/28 Javascript
jquery实现简易的移动端验证表单
2015/11/08 Javascript
基于jquery实现全屏滚动效果
2015/11/26 Javascript
javascript设计模式Constructor(构造器)模式
2016/08/19 Javascript
AngularJS创建自定义指令的方法详解
2016/11/03 Javascript
js获取当前页的URL与window.location.href简单方法
2017/02/13 Javascript
微信小程序本地缓存数据增删改查实例详解
2017/05/24 Javascript
Electron 如何调用本地模块的方法
2019/02/01 Javascript
详解iview的checkbox多选框全选时校验问题
2019/06/10 Javascript
vue实现拖拽的简单案例 不超出可视区域
2019/07/25 Javascript
JavaScript canvas绘制折线图
2020/02/18 Javascript
js实现坦克大战游戏
2020/02/24 Javascript
vue使用swiper实现左右滑动切换图片
2020/10/16 Javascript
[35:34]Liquid vs Winstrike 2018国际邀请赛小组赛BO2 第一场 8.18
2018/08/19 DOTA
[57:36]DOTA2-DPC中国联赛 正赛 SAG vs CDEC BO3 第三场 2月1日
2021/03/11 DOTA
python 删除大文件中的某一行(最有效率的方法)
2017/08/19 Python
python使用Tkinter实现在线音乐播放器
2018/01/30 Python
Python发送邮件功能示例【使用QQ邮箱】
2018/12/04 Python
对python while循环和双重循环的实例详解
2019/08/23 Python
Python下利用BeautifulSoup解析HTML的实现
2020/01/17 Python
Python3 hashlib密码散列算法原理详解
2020/03/30 Python
使用ITK-SNAP进行抠图操作并保存mask的实例
2020/07/01 Python
利用html5 file api读取本地文件示例(如图片、PDF等)
2018/03/07 HTML / CSS
美国时尚配饰品牌:Dooney & Bourke
2017/11/14 全球购物
澳大利亚体育和露营装备在线/实体零售商:Find Sports
2020/06/03 全球购物
大学生职业规划论文
2014/01/11 职场文书
行政专员岗位职责范本
2014/08/26 职场文书
贪污检举信范文
2015/03/02 职场文书
Python趣味爬虫之用Python实现智慧校园一键评教
2021/05/28 Python
MySQL快速插入一亿测试数据
2021/06/23 MySQL