为什么你还不懂得怎么使用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 控制语句
Nov 03 Python
Python模拟登录12306的方法
Dec 30 Python
用python 制作图片转pdf工具
Jan 30 Python
Python 类与元类的深度挖掘 II【经验】
May 06 Python
利用python实现简单的邮件发送客户端示例
Dec 23 Python
DataFrame中去除指定列为空的行方法
Apr 08 Python
Python使用Matplotlib模块时坐标轴标题中文及各种特殊符号显示方法
May 04 Python
Django 忘记管理员或忘记管理员密码 重设登录密码的方法
May 30 Python
Python 运行.py文件和交互式运行代码的区别详解
Jul 02 Python
Flask-WTF表单的使用方法
Jul 12 Python
Python交互环境下打印和输入函数的实例内容
Feb 16 Python
python实现凯撒密码、凯撒加解密算法
Jun 11 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
微信公众平台开发实现2048游戏的方法
2015/04/15 PHP
PHP读取Excel内的图片(phpspreadsheet和PHPExcel扩展库)
2019/11/19 PHP
用Javascript读取中文COOKIE的解决办法
2007/02/15 Javascript
判断客户端浏览器是否安装了Flash插件的多种方法
2010/08/11 Javascript
基于jquery的DIV随滚动条滚动而滚动的代码
2012/07/20 Javascript
javascript:文字不间断向左移动的实例代码
2013/08/08 Javascript
为jquery的ajaxfileupload增加附加参数的方法
2014/03/04 Javascript
用jquery.sortElements实现table排序
2014/05/04 Javascript
纯javascript判断查询日期是否为有效日期
2015/08/24 Javascript
jQuery zTree加载树形菜单功能
2016/02/25 Javascript
利用js编写响应式侧边栏
2016/09/17 Javascript
JavaScript表单验证的两种实现方法
2017/02/11 Javascript
JavaScript实现定时页面跳转功能示例
2017/02/14 Javascript
Bootstrap4 gulp 配置详解
2019/01/06 Javascript
详解vue项目中使用token的身份验证的简单实践
2019/03/08 Javascript
关于vue-cli 3配置打包优化要点(推荐)
2019/04/22 Javascript
ES6中Promise的使用方法实例总结
2020/02/18 Javascript
详解用js代码触发dom事件的实现方案
2020/06/10 Javascript
Vue中正确使用Element-UI组件的方法实例
2020/10/13 Javascript
python中lambda与def用法对比实例分析
2015/04/30 Python
Python定义函数功能与用法实例详解
2019/04/08 Python
python 实现在无序数组中找到中位数方法
2020/03/03 Python
Bealls Florida百货商店:生活服饰、家居装饰和鞋子
2018/02/23 全球购物
什么是Linux虚拟文件系统VFS
2012/01/31 面试题
Ajax主要包含了哪些技术
2014/06/12 面试题
介绍一下Python下range()函数的用法
2013/11/07 面试题
过滤器的用法
2013/10/08 面试题
应届毕业生个人求职自荐信
2014/01/06 职场文书
绿色学校实施方案
2014/03/31 职场文书
学校党的群众路线教育实践活动总结报告
2014/07/03 职场文书
2014年涉外离婚协议书范本
2014/11/20 职场文书
2014年度个人工作总结范文
2015/03/09 职场文书
最感人的道歉情书
2015/05/12 职场文书
庆元旦主持词
2015/07/06 职场文书
redis cluster支持pipeline的实现思路
2021/06/23 Redis
Python+Selenium实现读取网易邮箱验证码
2022/03/13 Python