背景介绍

在前面的篇章中介绍了同步和异步在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的方式定义协程

yield 简介

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的基础使用。

next 取值

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取值会报错

send 发送值

调用包含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. 驱动生成器执行

启动生成器

生成器创建之后需要启动才能返回值,也就从代码第一行执行到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

生成器的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就是返回值。

生成器实现的协程

生成器通常用作迭代器,但是也可以用作协程。协程其实就是生成器函数,通过主体中含有 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抢占式。

Python 异步编程之yield关键字的更多相关文章

  1. python异步编程之asyncio

    python异步编程之asyncio   前言:python由于GIL(全局锁)的存在,不能发挥多核的优势,其性能一直饱受诟病.然而在IO密集型的网络编程里,异步处理比同步处理能提升成百上千倍的效率, ...

  2. python异步编程之asyncio(百万并发)

      前言:python由于GIL(全局锁)的存在,不能发挥多核的优势,其性能一直饱受诟病.然而在IO密集型的网络编程里,异步处理比同步处理能提升成百上千倍的效率,弥补了python性能方面的短板,如最 ...

  3. python函数式编程之yield表达式形式

    先来看一个例子 def foo(): print("starting...") while True: res = yield print("res:",res ...

  4. python并发编程之asyncio协程(三)

    协程实现了在单线程下的并发,每个协程共享线程的几乎所有的资源,除了协程自己私有的上下文栈:协程的切换属于程序级别的切换,对于操作系统来说是无感知的,因此切换速度更快.开销更小.效率更高,在有多IO操作 ...

  5. 异步编程之Generator(1)——领略魅力

    异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...

  6. Python 多进程编程之multiprocessing--Pool

    Python 多进程编程之multiprocessing--Pool ----当需要创建的子进程数量不多的时候,可以直接利用multiprocessing 中的Process 动态生成多个进程, -- ...

  7. Python 多进程编程之multiprocessing--Process

    Python 多进程编程之multiprocessing 1,Process 跨平台的进程创建模块(multiprocessing), 支持跨平台:windowx/linux 创建和启动      创 ...

  8. python并发编程之gevent协程(四)

    协程的含义就不再提,在py2和py3的早期版本中,python协程的主流实现方法是使用gevent模块.由于协程对于操作系统是无感知的,所以其切换需要程序员自己去完成. 系列文章 python并发编程 ...

  9. python并发编程之multiprocessing进程(二)

    python的multiprocessing模块是用来创建多进程的,下面对multiprocessing总结一下使用记录. 系列文章 python并发编程之threading线程(一) python并 ...

  10. 异步编程之co——源码分析

    异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...

随机推荐

  1. WebSocket魔法师:打造实时应用的无限可能

    1.背景 在开发一些前端页面的时候,总是能接收到这样的需求:如何保持页面并实现自动更新数据呢?以往的常规做法,是前端使用定时轮询后端接口,获取响应后重新渲染前端页面,这种做法虽然能达到类似的效果,但是 ...

  2. P2360 地下城主

    题目大意 背景是逃离\(3D\)地下监狱,也就是三维样例,你可以前往所在小格的前方,后方,左方,右方,上层,下层的小格,'.'表示可走,'x'表示墙壁,'S'表示起点,'E'表示终点.每走一小格花费一 ...

  3. CLion安装与配置教程

    一.下载并安装CLion 1.下载 1.官网: Download CLion 2.注意: 这里建议使用2021.1.3版本之前,为之后的使用便利而做打算. (这里以Windows系统为例,其他系统类似 ...

  4. springboot的缓存和redis缓存,入门级别教程

    一.springboot(如果没有配置)默认使用的是jvm缓存 1.Spring框架支持向应用程序透明地添加缓存.抽象的核心是将缓存应用于方法,从而根据缓存中可用的信息减少执行次数.缓存逻辑是透明地应 ...

  5. springMvc_快速入门

    概念:是一种基于Java实现mvc模型的轻量级web框架 优点:使用简单,开发便捷    灵活性强 总体来说springMvc就是来替代servlet的一种工具 快速入门: 1.创建maven-web ...

  6. k8s~ingress_service_endpoint_pod四壮士

    在Kubernetes中,Service和Endpoints是两个重要的概念,它们之间存在着密切的关系. Service:Service是Kubernetes中用于定义一组Pod的访问方式的抽象.通过 ...

  7. 用CloudDrive将阿里云、天翼云、115挂载为电脑本地硬盘

    用CloudDrive将阿里云.天翼云.115挂载为电脑本地硬盘 写在前面 它可以把各大在线网盘,直接变成电脑本地硬盘,俗称"网盘挂载".这在一定程度上,确实能缓解存储紧张问题,瞬 ...

  8. f-VAEGAN-D2:VAE+GAN处理零样本学习问题

    虽然f-VAEGAN-D2在题目中说"适用任意样本",但对比的Few-shot相关的实验较少,这里仅讨论零样本学习的情况. 1. 背景介绍 由于为每个对象收集足够数量的高质量带标签 ...

  9. k8s初始化pod-pod标签

    目录 initContainers(初始化容器) 静态pod pod的调度策略(将pod指派给特定节点) initContainers(初始化容器) k8s在1.3版本的时候引入了一个初始化容器(in ...

  10. 斯坦福 UE4 C++ ActionRoguelike游戏实例教程 15.创建持续效果BUFF

    斯坦福课程 UE4 C++ ActionRoguelike游戏实例教程 0.绪论 概述 本篇文章对应Lecture 18 – Creating Buffs, World Interaction, 71 ...