Python 异步编程之yield关键字

image

1|0背景介绍


在前面的篇章中介绍了同步和异步在IO上的对比,从本篇开始探究python中异步的实现方法和原理。
python协程的发展流程:

  • python2.5 为生成器引用.send()、.throw()、.close()方法
  • python3.3 为引入yield from,可以接收返回值,可以使用yield from定义协程
  • Python3.4 加入了asyncio模块
  • Python3.5 增加async、await关键字,在语法层面的提供支持
  • python3.7 使用async def + await的方式定义协程
  • 此后asyncio模块更加完善和稳定,对底层的API进行的封装和扩展
  • python将于 3.10版本中移除 以yield from的方式定义协程

2|0yield 简介


yield 通常是使用在生成器函数中。当一个函数中有yield关键字,那么该函数就不是一个普通函数而是一个生成器函数。

>>> def get_num(): ... for i in range(5): ... yield i ... >>> g = get_num() >>> type(g) <class 'generator'> >>> >>> for i in g: ... print(i) ... 0 1 2 3 4

调用get_num生成了一个生成器g,通过type可以看到g是一个生成器类型。生成器是一种迭代器,可以通过for循环迭代出数据。

以上是yield的第一种使用方法,其实yeild出了可以作为生成器的关键字,也可以实现协程。在实现协程之前首先需要学习yield的基础使用。

3|0next 取值


yield实现的生成器是一种迭代器,所有的迭代器都可以通过next取值。

>>> def get_num(): ... for i in range(5): ... yield i ... >>> g = get_num() >>> >>> next(g) 0 >>> next(g) 1 >>> next(g) 2 >>> next(g) 3 >>> next(g) 4 >>> next(g) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration >>>

执行过程
使用next会让get_num从头开始执行,遇到yield i时,返回i给调用者,并暂停在yield i这一行,等待下一个next取值的到来,从yield i这里继续执行。这种能够暂停执行,恢复执行的能力是生成器的一种特性,也就是这种特性可以实现协程。
特点:
使用next取值有两个特点:

  1. 只能从前向后取值,每次只能取一个
  2. 迭代器中没有值时再通过next取值会报错

4|0send 发送值


调用包含yield的函数会返回一个生成器generator,可以通过next从生成器中不断取值,通过send也可以将数值送到生成器中。如下:

>>> def get_num(): ... for i in range(5): ... temp = yield i ... print(temp) ... >>> g = get_num() >>> next(g) 0 >>> g.send(100) 100 1 >>>

执行过程
next让程序执行到temp = yield i,返回i给调用者并暂停在这里。g.send(100) 从temp = yield i开始执行,将100传递给yield i,并让代码继续执行直到遇到下一个yield,返回yield 后面的数值。

send可以将值传递给生成器,next是从生成器中取值,两者目的不一致,但是也相同的能力,那就是可以驱动程序从一个yield执行到下一个yield。如再次执行send,就会从上一次暂停的地方继续执行到下一个yield处。

>>> g = get_num() >>> next(g) 0 >>> g.send(100) 100 1 >>> g.send(200) 200 2

特点

  1. 将值传送到生成器中
  2. 驱动生成器执行

5|0启动生成器


生成器创建之后需要启动才能返回值,也就从代码第一行执行到yield处,需要一个事件去驱动代码执行。两种方式可以启动,分别是send和next

>>> g = get_num() >>> next(g) 0 >>> g = get_num() >>> g.send(None) 0

next:
程序从第一行执行到 temp = yield i暂停。
send:
send()必须传入关键字None,其他值会报错。因为send是从yield 处开始执行,由于启动程序不是yield语句开始,所有不能传值。
close 结束迭代
通常来说不要手动接受生成器,因为生成器迭代完成之后就会被释放。但是也可以通过close的方法结束生成器的迭代。

>>> g = get_num() >>> g.send(None) 0 >>> >>> >>> >>> g = get_num() >>> >>> next(g) 0 >>> next(g) None 1 >>> g.send(100) 100 2 >>> g.close() >>> g.close() >>> next(g) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration

6|0生成器的return


在前面的示例中或许你也发现了,使用next取值,当取完所有元素之后再次取值时会抛出异常。根本原因是程序执行到return了。在生成器中执行到return会抛出StopIteration异常

>>> def gen(): ... yield 100 ... return 200 ... yield 300 ... >>> g = gen() >>> next(g) 100 >>> next(g) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration: 200 >>> next(g) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration 程序执行到return时会抛出StopIteration异常,终止程序。yield 300永远也不会执行。 调用生成器中无法直接获取到return的返回值,可以通过捕获异常的方法获取return的返回值。 >>> try: ... next(g) ... next(g) ... except StopIteration as e: ... result = e.value ... print(result) ... 100 200

使用try except 捕获 StopIteration 异常之后,从中取出value就是返回值。

7|0生成器实现的协程


生成器通常用作迭代器,但是也可以用作协程。协程其实就是生成器函数,通过主体中含有 yield 关键字的函数创建。注意这里只是为了探究原理,真实情况下协程不是使用yield。
协程的类似于函数调用,函数A调用函数B,B执行完成之后A继续执行。这个过程不涉及CPU调度。
下面通过生产者,消费者模型来说明yield如何实现协程。

import time def consume(): r = '' while True: n = yield r print(f'[consumer] 开始消费 {n}...') time.sleep(1) r = f'{n} 消费完成' def produce(c): next(c) n = 0 while n < 5: n = n + 1 print(f'[producer] 生产了 {n}...') r = c.send(n) print(f'[producer] consumer return: {r}') c.close() if __name__=='__main__': c = consume() produce(c)

执行输出:

[producer] 生产了 1... [consumer] 开始消费 1... [producer] consumer return: 1 消费完成 [producer] 生产了 2... [consumer] 开始消费 2... [producer] consumer return: 2 消费完成 [producer] 生产了 3... [consumer] 开始消费 3... [producer] consumer return: 3 消费完成 [producer] 生产了 4... [consumer] 开始消费 4... [producer] consumer return: 4 消费完成 [producer] 生产了 5... [consumer] 开始消费 5... [producer] consumer return: 5 消费完成

执行过程:

  1. c = consume() 创建消费者生成器
  2. produce(c)将消费者生成器传递到生产者函数中,生产者会负责驱动消费者
  3. next(c)驱动生产者启动,send(None)也可以完成
  4. 生产者在while中自增n,并调用 r = c.send(n) 将n传递给消费者
  5. 消费者n = yield r接收到n,用time.sleep模拟睡眠,给返回值r赋值,运行到下一个n = yield r暂停,返回r给生产者
  6. 生产者从暂定的r = c.send(n)恢复执行
  7. 直到n<5,生产者退出之后,整个协程退出。

这个过程中,主要配合的就是r = c.send(n)和 n = yield r这两个关键点。两行代码在执行时可以暂停,驱动另外一个执行。这里体现的协程的一个特点:主动让出CPU,协助式执行而不是线程那种CPU抢占式。
image


__EOF__

本文作者goldsunshine
本文链接https://www.cnblogs.com/goldsunshine/p/17902321.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   金色旭光  阅读(480)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示