为什么你还不懂得怎么使用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读取注册表中值的方法
Apr 08 Python
Python selenium如何设置等待时间
Sep 15 Python
使用python画个小猪佩奇的示例代码
Jun 06 Python
使用Python进行QQ批量登录的实例代码
Jun 11 Python
Python单元测试简单示例
Jul 03 Python
Dlib+OpenCV深度学习人脸识别的方法示例
May 14 Python
Python3简单实现串口通信的方法
Jun 12 Python
python opencv实现证件照换底功能
Aug 19 Python
使用Python 自动生成 Word 文档的教程
Feb 13 Python
解决python调用自己文件函数/执行函数找不到包问题
Jun 01 Python
Python getattr()函数使用方法代码实例
Aug 10 Python
Python日志器使用方法及原理解析
Sep 27 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
用PHP编写和读取XML的几种方式
2013/01/12 PHP
深入php常用函数的使用汇总
2013/06/08 PHP
destoon实现商铺管理主页设置增加新菜单的方法
2014/06/26 PHP
ThinkPHP中的常用查询语言汇总
2014/08/22 PHP
PHP使用token防止表单重复提交的方法
2016/04/07 PHP
php实现的统计字数函数定义与使用示例
2017/07/26 PHP
两个Javascript小tip资料
2010/11/23 Javascript
复制js对象方法(详解)
2013/07/08 Javascript
如何调试异步加载页面里包含的js文件
2014/10/30 Javascript
jQuery中element选择器用法实例
2014/12/29 Javascript
JavaScript中对象property的删除方法介绍
2014/12/30 Javascript
jQuery实现鼠标单击网页文字后在文本框显示的方法
2015/05/06 Javascript
一个简单不报错的summernote 图片上传案例
2016/07/11 Javascript
深入理解Angular.JS中的Scope继承
2017/06/04 Javascript
JS+HTML+CSS实现轮播效果
2017/11/28 Javascript
[00:44]华丽开场!DOTA2勇士令状带来全新对阵画面
2019/05/15 DOTA
linux下安装easy_install的方法
2013/02/10 Python
Python ZipFile模块详解
2013/11/01 Python
Python编程django实现同一个ip十分钟内只能注册一次
2017/11/03 Python
python爬虫正则表达式之处理换行符
2018/06/08 Python
python多线程并发让两个LED同时亮的方法
2019/02/18 Python
DataFrame.to_excel多次写入不同Sheet的实例
2019/12/02 Python
解决Tensorboard 不显示计算图graph的问题
2020/02/15 Python
如何用python爬取微博热搜数据并保存
2021/02/20 Python
html5拖曳操作 HTML5实现网页元素的拖放操作
2013/01/02 HTML / CSS
HTML5实现多张图片上传功能
2016/03/11 HTML / CSS
国际鲜花速递专家:Floraqueen
2016/11/24 全球购物
Vans奥地利官方网站:美国原创极限运动潮牌
2018/09/30 全球购物
Java模拟试题
2014/11/10 面试题
软件工程师岗位职责
2013/11/16 职场文书
适用于所有创业者的创业计划书
2014/02/05 职场文书
禁烟标语大全
2014/06/11 职场文书
军事博物馆观后感
2015/06/05 职场文书
同事欢送会致辞
2015/07/31 职场文书
六一儿童节致辞稿(3篇)
2019/07/11 职场文书
Win11黑色桌面背景怎么办?Win11黑色壁纸解决方法汇总
2022/04/05 数码科技