生成器(yield)作为协程

yield实际上是生成器,在python 2.5中,为生成器增加了.send(value)方法。这样调用者可以使用send方法对生成器发送数据,发送的数据在生成器中会赋值给yield左侧的变量(如果有的话),可以生成器可以作为协程使用。

下面是一个使用生成器实现的,求平均值的函数

  1. def averager1():
  2. """
  3. 使用yield接收数值,并求平均值
  4. :return:
  5. """
  6. count = 0
  7. total = 0.0
  8. average = 0.0
  9. while True:
  10. value = yield average
  11. count += 1
  12. total += value
  13. average = total/count
  14.  
  15. avg1 = averager1()
  16. # 预激活协程,程序执行到yield出暂停,产出average,输出0.0
  17. print(next(avg1))
  18. # 0.00
  19. # 向协程发送数字
  20. print(avg1.send(10))
  21. # 10.0
  22. print(avg1.send(20))
  23. # 15.0
  24. print(avg1.send(30))
  25. # 20.0

这里yield可以理解为连接调用者和生成器的运输小车,只不过运输的不是货物,而是数据。预激活生成器时,相当于调用者打电话给生成器,让他把小车开到调用者这里等待接收货物(代码执行到yield处暂停),这个时候,如果生成器有什么货物(数据)需要运输给调用者,那么可以顺带把货物捎带过去(yield average 产出值),当然也有可能是空车驶到调用者这里(yield右侧没有产出任何变量)。

接着,调用者需要将货物(数据)运输给生成器,那么就是重新让小车把货物运送给生成器(调用生成器的.send()方法)。生成器接收到yield小车运输过来的货物之后(value = yield),总之可以开始生产、销售或者其他事情(求平均值)。当生成器这些事情都做完后,又重新将小车开回到调用者这一方,并暂停,等待接收调用者的下一个指令,如此往返。

逐行解读上一个例子的代码

首先,调用生成器函数,创建一个生成器对象avg。然后对生成器对象进行预激活,这里的预激活指的是让生成器对象执行到yield除,然后会暂停。因为生成器对象只有在yield除暂停时,才能接收到调用者通过send方法发送给生成器的值,所以没有进行预激活的生成器对象无法正常工作。

我们知道,python的赋值语句,是从右向左执行的,所以在这里例子中,yield average会先执行,将average产出。

这个时候程序的控制器转交给调用者,继续执行print语句,所以print(next(avg))会输出yield产出的值,即average,输出0.00。

生成器的调用者继续往下执行,调用生成器的send方法,将10发送给生成器对象。开头说过,调用生成器对象的send方法,发送的数据,会赋值给yield左边的变量。在这个例子中,value = yield average, 暂时先把yield右侧的average忽略掉,只看value = yield,可以理解为,将yield获取到了调用者通过send方法发送给生成器的值,然后把这个值赋值给左侧的变量value,接着是简单的计算平均值,这样就实现了调用者给生成器对象发送数据。

注意生成器内部有个while的无限循环,生成器内部的代码会继续执行,直到再次遇到yield,程序暂停,再次把average的值产出,并将程序的控制器再次转交回调用者。

这个时候调用者继续执行print语句,输出average的值:10.0。后面的几次send也是一样的效果。

终止生成器对象的循环

之前的例子,while True会导致生成器对象无限循环,每次都会在yield除暂停,产出平均值average,并等待接收调用者再次通过send方法传入的新值。

如果需要终止生成器对象的无限循环,可以用三种方式:

  • 发送哨符终止循环

  • 调用生成器的.throw()方法终止循环

  • 调用生成器的.close()方法终止循环

发送哨符终止循环

从最简单的发送哨符终止循环开始,简单的说,就是发送一个特定的值给生成器对象,当生成器获取到这个值时,就通过break语句退出while循环。

  1. def averager2():
  2. """
  3. 使用yield接收数值,并求平均值
  4. 相对于上面的例子,增加了使协程退出的哨符
  5. :return:
  6. """
  7. count = 0
  8. total = 0.0
  9. average = 0.0
  10. while True:
  11. value = yield average
  12. # 当value为None时,退出循环
  13. if value is None:
  14. break
  15. count += 1
  16. total += value
  17. average = total/count
  18.  
  19. avg2 = averager2()
  20. # 预激活协程,因为yield右边没有变量,所以不会产出值
  21. print(next(avg2))
  22. # 0.0
  23. # 向协程发送数字
  24. print(avg2.send(10))
  25. # 10.0
  26. print(avg2.send(20))
  27. # 15.0
  28. print(avg2.send(30))
  29. # 20.0
  30. # 生成器循环终止时会抛出StopIteration
  31. # 所以做一个异常捕获
  32. try:
  33. avg2.send(None)
  34. except StopIteration:
  35. pass

上面的生成器函数,做了一个简单的判断,当value为None时,就执行break语句退出生成器对象的循环。生成器循环终止时会抛出StopIteration,这个也会作为后面生成器返回值的途径。

调用.throw()方法终止循环

调用生成器的.throw()方法,会将异常发送给生成器,生成器的处理规则如下:

  1. 生成器在yield处暂停时,会接收到throw方法传入的异常

  2. 如果生成器能正确处理传入的异常,那么生成器的代码会继续执行,yield产出右侧的值(如果右侧有值的话),并作为调用者调用生成器.throw()方法的返回值

  3. 如果生成器不能处理传入的异常,那么生成器的代码会中止运行,并将异常向上冒泡,再次发给调用者

看一个例子

  1. # 对第一个函数averager1进行修改,增加处理ValueError的代码
  2. def averager3():
  3. """
  4. 使用yield接收数值,并求平均值
  5. 对第一个函数averager3进行修改,增加处理ValueError的代码
  6. :return:
  7. """
  8. count = 0
  9. total = 0.0
  10. average = 0.0
  11. while True:
  12. try:
  13. value = yield average
  14. count += 1
  15. total += value
  16. average = total/count
  17. except ValueError:
  18. # 如果捕获到ValueError,什么都不做
  19. # 这样生成器会继续循环,直到再次遇到yield暂停
  20. pass
  21.  
  22. avg3 = averager3()
  23. next(avg3)
  24. print(avg3.send(10))
  25. # 10.0
  26. print(avg3.send(20))
  27. # 15.0
  28. # throw一个生成器可以处理的异常ValueError,没有任何影响
  29. # 生成器会继续运行,产出average,因为在yield处就报错,后续的代码没有执行
  30. # 所以average仍然为15.0
  31. # yield会将average产出,产出的值作为调用者执行生成器的throw方法的返回值,最终输出15.0
  32. print(avg3.throw(ValueError))
  33. # 15.0
  34. # throw一个生成器不能处理的异常,生成器循环终止
  35. try:
  36. print(avg3.throw(TypeError))
  37. except TypeError:
  38. print('生成器无法处理TypeError,异常向上冒泡抛出,循环终止')

调用.close()方法终止循环

close()方法,实际上是让生成器在yield出抛出GeneratorExit异常。

不过和直接.throw(GeneratorExit)不同的是,通过close让生成器抛出GeneratorExit后,生成器不能再产出任何值,否则会引发RuntimeError: generator ignored GeneratorExit。

  1. # 对第三个函数averager3进行修改,改为捕获GeneratorExit异常并忽略
  2. def averager4():
  3. """
  4. 使用yield接收数值,并求平均值
  5. 对第三个函数averager3进行修改,改为捕获GeneratorExit异常并忽略
  6. :return:
  7. """
  8. count = 0
  9. total = 0.0
  10. average = 0.0
  11. while True:
  12. try:
  13. value = yield average
  14. count += 1
  15. total += value
  16. average = total/count
  17. except GeneratorExit:
  18. # 如果捕获到GeneratorExit,什么都不做
  19. # 这样生成器会继续循环,直到再次遇到yield
  20. # 因为调用close后不允许再次yield,所以会抛出
  21. # RuntimeError: generator ignored GeneratorExit
  22. pass
  23. avg4 = averager4()
  24. next(avg4)
  25. print(avg4.send(10))
  26. print(avg4.send(20))
  27. avg4.close()
  28. # RuntimeError: generator ignored GeneratorExit

如果是直接.throw(GeneratorExit),那么遵循上述的规范,如果生成器处理了这个异常,循环继续;如果生成器无法的处理这个异常,循环终止。

通常情况下,生成器不应该捕获这个异常,或者捕获这个异常后应抛出StopItreation异常,否则调用方会报错。

协程返回值

协程是通过抛出StopIteration来返回值,StopIteration第一个值就是异常的返回值。

  1. def averager5():
  2. """
  3. 使用yield接收数值,并求平均值
  4. 修改averager2,每次yield不再产出平均数
  5. 而是改为协程结束后再返回
  6. :return:
  7. """
  8. count = 0
  9. total = 0.0
  10. average = 0.0
  11. while True:
  12. value = yield
  13. # 当value为None时,退出循环
  14. if value is None:
  15. break
  16. count += 1
  17. total += value
  18. average = total/count
  19. return average
  20.  
  21. avg5 = averager5()
  22. next(avg5)
  23. avg5.send(10)
  24. avg5.send(20)
  25. try:
  26. # 发送None,结束协程,同时捕获StopIteration异常
  27. avg5.send(None)
  28. except StopIteration as ex:
  29. print(ex)
  30. #

注:

《流畅的Python》学习笔记,部分例子来自书中,并有一些修改,便于验证某些结论。

Python协程笔记 - yield的更多相关文章

  1. 终结python协程----从yield到actor模型的实现

    把应用程序的代码分为多个代码块,正常情况代码自上而下顺序执行.如果代码块A运行过程中,能够切换执行代码块B,又能够从代码块B再切换回去继续执行代码块A,这就实现了协程 我们知道线程的调度(线程上下文切 ...

  2. 再议Python协程——从yield到asyncio

    协程,英文名Coroutine.前面介绍Python的多线程,以及用多线程实现并发(参见这篇文章[浅析Python多线程]),今天介绍的协程也是常用的并发手段.本篇主要内容包含:协程的基本概念.协程库 ...

  3. 理解Python协程:从yield/send到yield from再到async/await

    Python中的协程大概经历了如下三个阶段:1. 最初的生成器变形yield/send2. 引入@asyncio.coroutine和yield from3. 在最近的Python3.5版本中引入as ...

  4. 从yield 到yield from再到python协程

    yield 关键字 def fib(): a, b = 0, 1 while 1: yield b a, b = b, a+b yield 是在:PEP 255 -- Simple Generator ...

  5. 用yield实现python协程

    刚刚介绍了pythonyield关键字,趁热打铁,现在来了解一下yield实现协程. 引用官方的说法: 与线程相比,协程更轻量.一个python线程大概占用8M内存,而一个协程只占用1KB不到内存.协 ...

  6. python协程--yield和yield from

    字典为动词“to yield”给出了两个释义:产出和让步.对于 Python 生成器中的 yield 来说,这两个含义都成立.yield item 这行代码会产出一个值,提供给 next(...) 的 ...

  7. 00.用 yield 实现 Python 协程

    来源:Python与数据分析 链接: https://mp.weixin.qq.com/s/GrU6C-x4K0WBNPYNJBCrMw 什么是协程 引用官方的说法: 协程是一种用户态的轻量级线程,协 ...

  8. Python进阶----异步同步,阻塞非阻塞,线程池(进程池)的异步+回调机制实行并发, 线程队列(Queue, LifoQueue,PriorityQueue), 事件Event,线程的三个状态(就绪,挂起,运行) ,***协程概念,yield模拟并发(有缺陷),Greenlet模块(手动切换),Gevent(协程并发)

    Python进阶----异步同步,阻塞非阻塞,线程池(进程池)的异步+回调机制实行并发, 线程队列(Queue, LifoQueue,PriorityQueue), 事件Event,线程的三个状态(就 ...

  9. python协程(yield、asyncio标准库、gevent第三方)、异步的实现

    引言 同步:不同程序单元为了完成某个任务,在执行过程中需靠某种通信方式以协调一致,称这些程序单元是同步执行的. 例如购物系统中更新商品库存,需要用"行锁"作为通信信号,让不同的更新 ...

随机推荐

  1. linux内核分析ELF文件分析实践报告

  2. 个人git链接和git学习心得总结

    个人git链接和git学习心得总结 个人git链接: https://github.com/hanzhaoyan Git 是 Linux 的创始人 Linus Torvalds 开发的开源和免费的版本 ...

  3. 第三个Sprint ------第二天

    主界面代码 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns: ...

  4. MYSQL两个数据库字符集保持一致问题

    参考这篇文章:https://lzw.me/a/mysql-charset.html 还有一篇官方文档:https://dev.mysql.com/doc/refman/5.7/en/charset. ...

  5. Java使用HTTPClient4.3开发的公众平台消息模板的推送功能

    代码引用,参考文章:http://www.cnblogs.com/feiyun126/p/4778556.html,表示感谢! package com.yuanchuangyun.cyb.manage ...

  6. Python 零基础 快速入门 趣味教程 (咪博士 海龟绘图 turtle) 4. 函数

    什么样的程序员才是优秀的程序员?咪博士认为“慵懒”的程序员才是真正优秀的程序员.听起来不合逻辑?真正优秀的程序员知道如何高效地工作,而不是用不止境的加班来完成工作任务.函数便是程序员高效工作的利器之一 ...

  7. 无需破解:Windows Server 2008 R2 至少免费使用 900天

    无需破解:Windows Server 2008 R2 至少免费使用 900天 2009年10月30日 星期五 02:10 1.首先安装后,有一个180天的试用期. 2.在180天试用期即将结束时,使 ...

  8. 改变自己从学习linux开始

    刚刚高中毕业,进如大学的时候,总以为摆脱了束缚可以无拘无束的玩耍了.当时真的就是和众多大学生一起,像撒欢的野马,每天逃课,上网,泡吧,不把学习当一会事,学校里教授讲的各种知识也没有听在心里,前两年玩的 ...

  9. 如何修改可运行Jar包,如何反编译Jar包

    将可运行Jar包,反编译成项目,修改代码,再次编译,打包. 需要工具:jd-gui.myeclipse 具体步骤: 1.使用jd-gui打开原始的Jar包,选择File-->Save All  ...

  10. MT【40】一道联赛二试题

    让我通过这道题来演示如何利用切比雪夫多项式的内功心法: 评:如此大道至简,当年为之叫绝的精彩的做法