本网站相关文章:
变化历史 版本 首先, 关于协程的优势已经在同步编程和异步编程 中介绍过, 这里不再阐述. 那么, Python 中协程的演变是怎样的呢?
版本
详细说明
2.2
引入生成器作为一种语言特性,通过yield
语句实现基础协程功能。
2.5
引入greenlet
库,通过手动控制协程切换实现更灵活的协程编程。
3.2
引入yield from
语法,简化协程编程的嵌套和异常处理。
3.3
引入asyncio
库,提供异步编程的基础设施,包括协程、事件循环和异步 IO 操作。
3.4
引入async
和await
关键字,作为定义协程函数和在协程中进行异步操作的语法糖。
3.5
引入asyncio
的async
和await
的原生语法支持。
3.6
引入asyncio
的asyncio.run()
函数,简化异步代码的入口点。
3.7
引入asyncio
的asyncio.create_task()
函数,简化任务的创建和管理。
3.8
引入asyncio
的asyncio.get_running_loop()
函数,获取当前运行的事件循环。
3.9
引入asyncio
的asyncio.to_thread()
函数,将阻塞操作移动到线程池中执行。
3.10
引入asyncio
的asyncio.all_tasks()
函数,获取所有当前运行的任务。
请注意,这些版本是根据 Python 的发布历史作出的概括,某些功能的添加和改进可能在不同的小版本中发生。其中 yield from 和 asyncio 库实际上存在时间极其短暂, 因为python3.0~python3.4
提出阶段, 使用 python3 的开发者实际上不是非常多.
yield 和 await yield from
和await
是用于在协程中等待另一个协程或可迭代对象的关键字,它们有一些区别和不同的语法使用:
语法差异 :yield from
是 Python 3.3 引入的语法,而await
是 Python 3.5 引入的语法。yield from
使用在生成器中,而await
使用在异步函数(包含async def
声明的函数)中。
表达方式 :yield from
用于委派生成器,它将控制权委托给另一个生成器,并能从中获取生成的值。await
用于等待可等待对象,它能挂起当前协程的执行,并等待另一个协程或异步操作的结果。
功能扩展 :await
语法更加灵活,它可以等待各种可等待对象,包括协程、异步函数、异步生成器和其他实现了__await__
特殊方法的对象。而yield from
则主要用于在生成器中委派子生成器。
异常处理 :yield from
能够自动处理委派生成器和子生成器之间的异常,异常会被透明地传递给委派生成器的调用方。而await
使用try/except
块来捕获和处理异常,通过try/except
语句块包裹await
表达式来处理可能发生的异常。
总体来说,yield from
和await
都用于在协程中等待其他的可迭代对象或协程,但在语法和使用方式上有一些差异。await
更加通用且灵活,是 Python 3.5 及以上版本中推荐使用的协程等待语法。
生成器和协程
a. 用途不同 :生成器主要用于生成一系列的值,可以通过yield
语句逐个产生值。它们通常用于迭代器的实现和惰性计算。协程则更加强调并发和异步编程,允许在协程之间进行切换和暂停,以便处理异步任务。
b. 调用方式不同 :生成器通过next()
函数或迭代器协议进行迭代,每次调用next()
会执行生成器中的代码,直到遇到yield
语句停止。协程通过事件循环或异步框架调度运行,可以被暂停、恢复和取消。
c. 状态维护不同 :生成器维护自己的状态,每次从yield
语句继续执行时会从上次停止的位置继续。协程可以通过await
语句暂停执行,并保留当前的上下文状态,下次恢复时会继续执行暂停的位置。
d. 通信方式不同 :生成器主要通过产生和消费值的方式进行通信,通过yield
语句从生成器中产生值,并可以通过函数参数或外部迭代器向生成器传递值。协程则更加灵活,可以在协程之间进行双向通信,通过await
语句等待其他协程的结果,并通过send()
方法向协程发送值。
需要注意的是,Python 中的协程通常是基于生成器实现的,通过使用yield
语句来实现协程的暂停和恢复。因此,协程可以看作是生成器的一种特殊形式,但协程拥有更多的特性和能力,用于处理并发和异步编程。
那么, 为何协程可以看成是生成器的一种特殊形式呢? 为何不直接使用 async/await 来实现协程呢?
实际上,Python 3.5 及以上版本引入了asyncio
模块,引入了新的async
和await
关键字,用于定义和管理协程。async
和await
提供了更简洁和直观的语法来编写和管理协程,使得异步编程更加易于理解和维护。
在早期版本的 Python 中,协程功能并没有原生支持。因此,通过使用生成器和yield
语句来模拟协程是一种常见的做法。生成器提供了协程所需的暂停和恢复执行的能力,但语法相对较为复杂。
随着 Python 的发展,引入了async/await
关键字,使得协程的编写和使用更加直观和易于理解。使用async/await
,我们可以更清晰地定义异步任务和操作,而不需要手动管理生成器和yield
语句。
因此,现代的 Python 代码通常会使用async/await
来定义和管理协程,而不是直接使用生成器和yield
语句。这样可以使代码更加简洁、易读和易于维护。但对于早期版本的 Python,使用生成器和yield
语句来模拟协程仍然是一种有效的方式。
一个早期的通过 yield 实现的协程案例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import timedef countdown (name, n ): print (f'{name} started' ) while n > 0 : print (f'{name} : T-minus {n} ' ) yield time.sleep(1 ) n -= 1 print (f'{name} finished' ) coroutine1 = countdown('Coro1' , 5 ) coroutine2 = countdown('Coro2' , 3 ) def scheduler (*coroutines ): while True : for coroutine in coroutines: try : next (coroutine) except StopIteration: coroutines.remove(coroutine) if not coroutines: break scheduler(coroutine1, coroutine2)
yield yield 用法
利用 yield 实现一个递减技术迭代器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def iterator_yield (num ): """ yield 组成的迭代器, 利用yield 不但返回, 一般来说这里value是一个IO操作 """ value = num while value > 0 : value = value - 1 yield value return if __name__ == '__main__' : for i in iterator_yield(5 ): print (i) it = iterator_yield(3 ) print ('测试next:{}' .format (next (it)))
此时协程有四种状态位:
GEN_CREATED
: 等待执行, 此时还未进入协程
GEN_RUNNING
: 解释器执行
GEN_SUSPENDED
: yield 处暂停等待
GEN_CLOSED
: 执行结束
利用 yield 抛出迭代值, 利用 send 传入 jump 值, 注意, 抛出值和传入值是不同的变量.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def iterator_yield (maxnum ): """ 传入一个最大值, 一旦迭代超过该值, 结束 """ step = 0 while step < maxnum: jump = yield step if jump is None : step += 1 else : print ('传入一个新的step值:{}' .format (jump)) step = step + jump if __name__ == '__main__' : it = iterator_yield(6 ) print (next (it)) it.send(3 ) print (next (it))
另外, 注意, 如果多次 send, 则会抛出异常. 在开始讲解 yield from 之前我们先简单的介绍下 yield 的缺点以对后面要讲解的 yield from 有一个清晰的认识:
a. 协程函数返回值获取不方便, 只能通过触发StopIteration
来最终返回异常的 value, 例如 for 循环中被语法糖自动处理
b. 无法进行 yield 嵌套或者实现复杂, 每次只能向外层 yield 一个值
c. 需要手动处理异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def bamboo_generator (): for _i in range (10 ): if _i == 5 : return '中断循环, 返回' yield _i def check_iterator_return (): """ 验证yield的缺点, 在for语法糖中无法获取函数返回值 """ print ('------------test iterator return---------------' ) try : for item in bamboo_generator(): print (item) except StopIteration as msg: print (msg)
注意, 上面的代码在 python2.7 下运行有误, 从执行结果可知, bamboo_generator
中的 return 返回结果不知道怎么获取.
yield from 利用 yield from 来使生成器能够很容易分为多个拥有 send 和 throw 方法的子生成器
, 类似一个大函数可以分为多个子函数一样简单. 该语法的使用格式: yield from {普通的可迭代对象, 迭代器, 生成器}
, 其实际返回两个值:
从子生成器返回的一个迭代对象, 被直接抛出给上层, 例如下面例子中直接抛给 main 层
yield from 表达式本身返回子生成器的 return 值, 例如下面例子中show_return
的 value 值, 其中 yield from 会自动捕获子生成器的 StopIteration 异常并返回
在整个交互管道上有几个相关的专业术语需要简单介绍下:
委派生成器: 包含yield from <iterable>
的生成器函数, 其相当于一个 wrap channel
子生成器: yield from
后面的生成器
调用方: 调用委派生成器的客户端
生成器链: 若子生成器本身也是一个 yield from 封装的生成器, 则可以将任意数量的委托派生器关联在一个链上
下面就是一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 def return_ex (): """ 测试yield from会返回其后生成器的 return 值 """ yield 'return_yield_1' yield 'return_yield_2' yield 'return_yield_3' return 'return_return_4' def show_return (): value = yield from return_ex() print ('生成器 1值:{}' .format (value)) yield 'End' def show_return_eq (): try : for item in return_ex(): yield item except StopIteration as exc: value = exec .value if __name__ == '__main__' : show = show_return() for v in show: print ('生成器 2 返回:{}' .format (v))
上面的代码输出结果如下:
1 2 3 4 5 生成器 2 返回:return_yield_1 生成器 2 返回:return_yield_2 生成器 2 返回:return_yield_3 生成器 1值:return_return_4 生成器 2 返回:End
再回顾 2.1 节 yield 缺点的例子, 如果使用 yield from 实现则效果如何呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 def bamboo_generator (): for _i in range (10 ): if _i == 5 : return '中断循环, 返回' yield _i def check_iterator_return (): """ 验证yield的缺点, 在for语法糖中无法获取函数返回值 """ print ('------------test iterator return---------------' ) try : for item in bamboo_generator(): print (item) except StopIteration as msg: print (msg) def wrap_bamboo_generator (generator ): """ 包装已有的生成器, 返回一个新的生成器 """ result = yield from generator print (f'被包装生成器结果:{result} ' ) def check_yield_from_return (): """ 验证yield from修复yield 无法返回return的问题 """ print ('------------test yield from return---------------' ) try : for item in wrap_bamboo_generator(bamboo_generator()): print (item) except StopIteration as msg: print (msg)
从结果可知, 通过封装已有的 generator 并返回该 generator 返回值最后打印出了 return 的返回值. 实际上yield from iterable
等价于for item in iterable: yield item
, 另外 yield from 还能作为一个中介机构, 作为双通道来串联多个生成器.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import jsonfrom collections import namedtupleResult = namedtuple('Result' , 'count average' ) def averager (): """ 子生成器: 求取平均值, 根据send过来的值不断进行累加并返回平均值 """ print ('Go into average generator...' ) count = total = 0.0 while True : term = yield 'test' if term is None : break total += term count += 1 return Result(count, total / count) def grouper (results, key ): """ 双管道, 会自动将main中send的值传递给averager, 在averager最终return之后才会返回表达式本身的值, 其他使用作为一个数据传输管道 """ results[key] = yield from averager() print ('---Grouper结束运行---' ) return '委托end' def main (_data ): """ 统计平均值入口函数 """ results = {} for key, values in _data.items(): try : group = grouper(results, key) next (group) for value in values: group.send(value) group.send(None ) except Exception as msg: print (F'委托生成器最终的返回值:{msg} ' ) print (json.dumps(results, indent=2 )) if __name__ == '__main__' : data = { 'girls;kg' : [40.9 , 38.5 , 44.3 , 42.2 , 45.2 , 41.7 , 44.5 , 38.0 , 40.6 , 44.5 ], 'girls;m' : [1.6 , 1.51 , 1.4 , 1.3 , 1.41 , 1.39 , 1.33 , 1.46 , 1.45 , 1.43 ], } main(data)
上面的代码说明以及yield from
中三个角色关系图如下:
async asyncio asyncio
是用来编写并发代码的库, 为构建 IO 密集型和网络连接代码提供了最佳的解决方案. 首先,让我们简单了解下 asyncio 中各个简单的概念:
event_loop, 事件循环, 了解过 libev 等事件库的都了解, 这是一个无限事件循环, 不断监听事件的监听器
coroutine, 协程对象, 可以通过内置的方法创建协程对象, 之后将协程对象注册到事件 event_loop 中.注意, 在 python3.4 中协程使用@asyncio.coroutine, 使用yield from
来驱动, 但是在 python3.5 中发生了一些变化: @asyncio.coroutine -> async
, yield from -> await
, 当然新版本仍然向后兼容.
Future, 表示尚未完成的计算或者结果, 或者表示为将来执行或还没有执行的任务
Task, Future 的子类, 运行某个任务的同时可以并发的运行多个任务, 其通过asyncio.async()
, loop.create_task()
, asyncio.ensure_future()
来创建.
下面是一个仅仅在python3.4
环境下的异步编写例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @asyncio.coroutine def countdown (number, n ): while n > 0 : print ('T-minus' , n, '({})' .format (number)) _start = time.time() yield from asyncio.sleep(1 ) _end = time.time() print (f'{number} --> {round (_end - _start, 3 )} ' ) n -= 1 loop = asyncio.get_event_loop() start = time.time() tasks = [ asyncio.ensure_future(countdown("A" , 2 )), asyncio.ensure_future(countdown("B" , 3 )), asyncio.ensure_future(countdown("c" , 2 )), asyncio.ensure_future(countdown("d" , 0 )), asyncio.ensure_future(countdown("e" , 5 )), asyncio.ensure_future(countdown("f" , 2 )), ] loop.run_until_complete(asyncio.wait(tasks)) loop.close() end = time.time() """ 输出说明: 1. 串行执行预期耗时: 2 + 3 + 2 + 0 + 5 + 2 == 14 2. 实际耗时: max(2, 3, 2, 0, 5, 2) == 5 所以协程并发缺失达到预期的目标, 将IO和CPU执行区分开来. """ print (f'整个过程总耗时:{int (end - start)} ' )
上面的代码输出如下:
1 2 3 4 ... T-minus 1 (e) e --> 1.002 整个过程总耗时:5
注意上面的asyncio.sleep
函数, 如果在异步协程
中出现同步模块相关的代码, 实际上无法实现异步编程. 例如在进行网络爬取的时候, 可以使用功能基于 asyncio 的库aiohttp
来进行网络请求.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import aiohttpimport asyncioimport async_timeoutasync def fetch (session, url ): with async_timeout.timeout(10 ): async with session.get(url) as response: return await response.text() async def main (): async with aiohttp.ClientSession() as session: html = await fetch(session, 'https://python.org' ) print (html) loop = asyncio.get_event_loop() loop.run_until_complete(main())
又比如在scrapy
中使用request
来请求网络资源就会浪费整个框架的异步设计.
async/await 在 Python3.5 之后, python 内置支持异步编程操作, 上面的测试例子可以改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import asyncioasync def countdown (number, n ): while n > 0 : print ('T-minus' , n, '({})' .format (number)) await asyncio.sleep(1 ) n -= 1 loop = asyncio.get_event_loop() tasks = [ asyncio.ensure_future(countdown("A" , 2 )), asyncio.ensure_future(countdown("B" , 3 )), asyncio.ensure_future(countdown("c" , 2 )), ] loop.run_until_complete(asyncio.wait(tasks)) loop.close()
注意, 更多关于 async, await 的例子说明见项目asyncio-ftwpd , 该项目中有非常详细的 asyncio 的使用实例.
参考