本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理

在 Python 开发中, yield 关键字的使用其实较为频繁,例如大集合的生成,简化代码结构、协程与并发都会用到它。

但是,你是否真正了解 yield 的运行过程呢?

这篇文章,我们就来看一下 yield 的运行流程,以及在开发中哪些场景适合使用 yield

生成器

如果在一个方法内,包含了 yield 关键字,那么这个函数就是一个「生成器」。

生成器其实就是一个特殊的迭代器,它可以像迭代器那样,迭代输出方法内的每个元素。

我们来看一个包含 yield 关键字的方法:

  1. # coding: utf8
  2.  
  3. # 生成器
  4. defgen(n):
  5. foriinrange(n):
  6. yieldi
  7.  
  8. g = gen(5)# 创建一个生成器
  9. print(g)# <generator object gen at 0x10bb46f50>
  10. print(type(g))# <type 'generator'>
  11.  
  12. # 迭代生成器中的数据
  13. foriing:
  14. print(i)
  15.  
  16. # Output:
  17. # 0 1 2 3 4

注意,在这个例子中,当我们执行 g = gen(5) 时, gen 中的代码其实并没有执行,此时我们只是创建了一个「生成器对象」,它的类型是 generator

然后,当我们执行 for i in g ,每执行一次循环,就会执行到 yield 处,返回一次 yield 后面的值。

这个迭代过程是和迭代器最大的区别。

换句话说,如果我们想输出 5 个元素,在创建生成器时,这个 5 个元素其实还并没有产生,什么时候产生呢?只有在执行 for 循环遇到 yield 时,才会依次生成每个元素。

此外,生成器除了和迭代器一样实现迭代数据之外,还包含了其他方法:

  • generator.__next__() :执行 for 时调用此方法,每次执行到 yield 就会停止,然后返回 yield 后面的值,如果没有数据可迭代,抛出 StopIterator 异常, for 循环结束

  • generator.send(value) :外部传入一个值到生成器内部,改变 yield 前面的值

  • generator.throw(type[, value[, traceback]]) :外部向生成器抛出一个异常

  • generator.close() :关闭生成器

通过使用生成器的这些方法,我们可以完成很多有意思的功能。

__ next __

先来看生成器的 __next__ 方法,我们看下面这个例子。

  1. # coding: utf8
  2.  
  3. defgen(n):
  4. foriinrange(n):
  5. print('yield before')
  6. yieldi
  7. print('yield after')
  8.  
  9. g = gen(3)# 创建一个生成器
  10. print(g.__next__())# 0
  11. print('----')
  12. print(g.__next__())# 1
  13. print('----')
  14. print(g.__next__())# 2
  15. print('----')
  16. print(g.__next__())# StopIteration
  17.  
  18. # Output:
  19. # yield before
  20. # 0
  21. # ----
  22. # yield after
  23. # yield before
  24. # 1
  25. # ----
  26. # yield after
  27. # yield before
  28. # 2
  29. # ----
  30. # yield after
  31. # Traceback (most recent call last):
  32. # File "gen.py", line 16, in <module>
  33. # print(g.__next__()) # StopIteration
  34. # StopIteration

在这个例子中,我们定义了 gen 方法,这个方法包含了 yield 关键字。然后我们执行 g = gen(3) 创建一个生成器,但是这次没有执行 for 去迭代它,而是多次调用 g.next() 去输出生成器中的元素。

我们看到,当执行 g.__next__() 时,代码就会执行到 yield 处,然后返回 yield 后面的值,如果继续调用 g.__next__() ,注意,你会发现,这次执行的开始位置,是上次 yield 结束的地方,并且它还保留了上一次执行的上下文,继续向后迭代。

这就是使用 yield 的作用,在迭代生成器时,每一次执行都可以保留上一次的状态,而不是像普通方法那样,遇到 return 就返回结果,下一次执行只能再次重复上一次的流程。

生成器除了能保存状态之外,我们还可以通过其他方式,改变其内部的状态,这就是下面要讲的 sendthrow 方法。

send

上面的例子中,我们只展示了在 yield 后有值的情况,其实还可以使用 j = yield i 这种语法,我们看下面的代码:

  1. # coding: utf8
  2.  
  3. defgen():
  4. i =1
  5. whileTrue:
  6. j =yieldi
  7. i *=2
  8. ifj ==-1:
  9. break

此时如果我们执行下面的代码:

  1. foriingen():
  2. print(i)
  3. time.sleep(1)

输出结果会是 1 2 4 8 16 32 64 ... 一直循环下去, 直到我们杀死这个进程才能停止。

这段代码一直循环的原因在于,它无法执行到 j == -1 这个分支里 break 出来,如果我们想让代码执行到这个地方,如何做呢?

这里就要用到生成器的 send 方法了, send 方法可以把外部的值传入生成器内部,从而改变生成器的状态。

代码可以像下面这样写:

  1. g = gen()# 创建一个生成器
  2. print(g.__next__())# 1
  3. print(g.__next__())# 2
  4. print(g.__next__())# 4
  5. # send 把 -1 传入生成器内部 走到了 j = -1 这个分支
  6. print(g.send(-1))# StopIteration 迭代停止

当我们执行 g.send(-1) 时,相当于把 -1 传入到了生成器内部,然后赋值给了 yield 前面的 j ,此时 j = -1 ,然后这个方法就会 break 出来,不会继续迭代下去。

throw

外部除了可以向生成器内部传入一个值外,还可以传入一个异常,也就是调用 throw 方法:

  1. # coding: utf8
  2.  
  3. defgen():
  4. try:
  5. yield1
  6. exceptValueError:
  7. yield'ValueError'
  8. finally:
  9. print('finally')
  10.  
  11. g = gen()# 创建一个生成器
  12. print(g.__next__())# 1
  13. # 向生成器内部传入异常 返回ValueError
  14. print(g.throw(ValueError))
  15.  
  16. # Output:
  17. # 1
  18. # ValueError
  19. # finally

这个例子创建好生成器后,使用 g.throw(ValueError) 的方式,向生成器内部传入了一个异常,走到了生成器异常处理的分支逻辑。

close

生成器的 close 方法也比较简单,就是手动关闭这个生成器,关闭后的生成器无法再进行操作。

  1. >>>g = gen()
  2. >>>g.close()# 关闭生成器
  3. >>>g.__next__()# 无法迭代数据
  4. Traceback (most recent call last):
  5. File"<stdin>", line1,in<module>
  6. StopIteration

close 方法我们在开发中使用得比较少,了解一下就好。

使用场景

了解了 yield 和生成器的使用方式,那么 yield 和生成器一般用在哪些业务场景中呢?

下面我介绍几个例子,分别是大集合的生成、简化代码结构、协程与并发,你可以参考这些使用场景来使用 yield

大集合的生成

如果你想生成一个非常大的集合,如果使用 list 创建一个集合,这会导致在内存中申请一个很大的存储空间,例如想下面这样:

  1. # coding: utf8
  2.  
  3. defbig_list():
  4. result = []
  5. foriinrange(10000000000):
  6. result.append(i)
  7. returnresult
  8.  
  9. # 一次性在内存中生成大集合 内存占用非常大
  10. foriinbig_list():
  11. print(i)

这种场景,我们使用生成器就能很好地解决这个问题。

因为生成器只有在执行到 yield 时才会迭代数据,这时只会申请需要返回元素的内存空间,代码可以这样写:

  1. # coding: utf8
  2.  
  3. defbig_list():
  4. foriinrange(10000000000):
  5. yieldi
  6.  
  7. # 只有在迭代时 才依次生成元素 减少内存占用
  8. foriinbig_list():
  9. print(i)

简化代码结构

我们在开发时还经常遇到这样一种场景,如果一个方法要返回一个 list ,但这个 list 是多个逻辑块组合后才能产生的,这就会导致我们的代码结构变得很复杂:

  1. # coding: utf8
  2.  
  3. defgen_list():
  4. # 多个逻辑块 组成生成一个列表
  5. result = []
  6. foriinrange(10):
  7. result.append(i)
  8. forjinrange(5):
  9. result.append(j * j)
  10. forkin[100,200,300]:
  11. result.append(k)
  12. returnresult
  13.  
  14. foritemingen_list():
  15. print(item)

这种情况下,我们只能在每个逻辑块内使用 appendlist 中追加元素,代码写起来比较啰嗦。

此时如果使用 yield 来生成这个 list ,代码就简洁很多:

  1. # coding: utf8
  2.  
  3. defgen_list():
  4. # 多个逻辑块 使用yield 生成一个列表
  5. foriinrange(10):
  6. yieldi
  7. forjinrange(5):
  8. yieldj * j
  9. forkin[100,200,300]:
  10. yieldk
  11.  
  12. foritemingen_list():
  13. print(i)

使用 yield 后,就不再需要定义 list 类型的变量,只需在每个逻辑块直接 yield 返回元素即可,可以达到和前面例子一样的功能。

我们看到,使用 yield 的代码更加简洁,结构也更清晰,另外的好处是只有在迭代元素时才申请内存空间,降低了内存资源的消耗。

协程与并发

还有一种场景是 yield 使用非常多的,那就是「协程与并发」。

如果我们想提高程序的执行效率,通常会使用多进程、多线程的方式编写程序代码,最常用的编程模型就是「生产者-消费者」模型,即一个进程 / 线程生产数据,其他进程 / 线程消费数据。

在开发多进程、多线程程序时,为了防止共享资源被篡改,我们通常还需要加锁进行保护,这样就增加了编程的复杂度。

在 Python 中,除了使用进程和线程之外,我们还可以使用「协程」来提高代码的运行效率。

什么是协程?

简单来说, 由多个程序块组合协作执行的程序,称之为「协程」。

而在 Python 中使用「协程」,就需要用到 yield 关键字来配合。

可能这么说还是太好理解,我们用 yield 实现一个协程生产者、消费者的例子:

  1. # coding: utf8
  2.  
  3. defconsumer():
  4. i =None
  5. whileTrue:
  6. # 拿到 producer 发来的数据
  7. j =yieldi
  8. print('consume %s'% j)
  9.  
  10. defproducer(c):
  11. c.__next__()
  12. foriinrange(5):
  13. print('produce %s'% i)
  14. # 发数据给 consumer
  15. c.send(i)
  16. c.close()
  17.  
  18. c = consumer()
  19. producer(c)
  20.  
  21. # Output:
  22. # produce 0
  23. # consume 0
  24. # produce 1
  25. # consume 1
  26. # produce 2
  27. # consume 2
  28. # produce 3
  29. # consume 3
  30. ...

这个程序的执行流程如下:

  1. c = consumer() 创建一个生成器对象

  2. producer(c) 开始执行, c.__next()__ 会启动生成器 consumer 直到代码运行到 j = yield i 处,此时 consumer 第一次执行完毕,返回

  3. producer 函数继续向下执行,直到 c.send(i) 处,这里利用生成器的 send 方法,向 consumer 发送数据

  4. consumer 函数被唤醒,从 j = yield i 处继续开始执行,并且接收到 producer 传来的数据赋值给 j ,然后打印输出,直到再次执行到 yield 处,返回

  5. producer 继续循环执行上面的过程,依次发送数据给 cosnumer ,直到循环结束

  6. 最终 c.close() 关闭 consumer 生成器,程序退出

在这个例子中我们发现,程序在 producerconsumer 这 2 个函数之间 来回切换 执行,相互协作,完成了生产任务、消费任务的业务场景,最重要的是,整个程序是在 单进程单线程 下完成的。

这个例子用到了上面讲到的 yield 、生成器的 __next__sendclose 方法。如果不好理解,你可以多看几遍这个例子,最好自己测试一下。

我们使用协程编写生产者、消费者的程序时,它的好处是:

  • 整个程序运行过程中无锁,不用考虑共享变量的保护问题,降低了编程复杂度

  • 程序在函数之间来回切换,这个过程是用户态下进行的,不像进程 / 线程那样,会陷入到内核态,这就减少了内核态上下文切换的消耗,执行效率更高

所以, Python 的 yield 和生成器实现了协程的编程方式,为程序的并发执行提供了编程基础。

Python 中的很多第三方库,都是基于这一特性进行封装的,例如 geventtornado ,它们都大大提高了程序的运行效率。

总结

总结一下,这篇文章我们主要讲了 yield 的使用方式,以及生成器的各种特性。

生成器是一种特殊的迭代器,它除了可以迭代数据之外,在执行时还可以保存方法中的状态,除此之外,它还提供了外部改变内部状态的方式,把外部的值传入到生成器内部。

利用 yield 和生成器的特性,我们在开发中可以用在大集成的生成、简化代码结构、协程与并发的业务场景中。

Python 的 yield 也是实现协程和并发的基础,它提供了协程这种用户态的编程模式,提高了程序运行的效率。

想要获取更多Python学习资料可以加
QQ:2955637827私聊
或加Q群630390733
大家一起来学习讨论吧!

Python 进阶——如何正确使用 yield?的更多相关文章

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

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

  2. python进阶篇

    python进阶篇 import 导入模块 sys.path:获取指定模块搜索路径的字符串集合,可以将写好的模块放在得到的某个路径下,就可以在程序中import时正确找到. ​ import sys ...

  3. Python进阶-继承中的MRO与super

    Python进阶-继承中的MRO与super 写在前面 如非特别说明,下文均基于Python3 摘要 本文讲述Python继承关系中如何通过super()调用"父类"方法,supe ...

  4. python进阶学习笔记(一)

    python进阶部分要学习的内容: 学习目标: 1.函数式编程 1.1,什么是函数式编程 函数式编程是一种抽象计算的编程模式 不同语言的抽象层次不同: 函数式编程的特点: python支持的函数式编程 ...

  5. Python进阶 函数式编程和面向对象编程等

    函数式编程 函数:function 函数式:functional,一种编程范式.函数式编程是一种抽象计算机的编程模式. 函数!= 函数式(如计算!=计算机) 如下是不同语言的抽象 层次不同 高阶函数: ...

  6. 【python进阶】深入理解系统进程2

    前言 在上一篇[python进阶]深入理解系统进程1中,我们讲述了多任务的一些概念,多进程的创建,fork等一些问题,这一节我们继续接着讲述系统进程的一些方法及注意点 multiprocessing ...

  7. Python进阶:全面解读高级特性之切片!

    导读:切片系列文章连续写了三篇,本文是对它们做的汇总.为什么要把序列文章合并呢?在此说明一下,本文绝不是简单地将它们做了合并,主要是修正了一些严重的错误(如自定义序列切片的部分),还对行文结构与章节衔 ...

  8. Python进阶:迭代器与迭代器切片

    2018-12-31 更新声明:切片系列文章本是分三篇写成,现已合并成一篇.合并后,修正了一些严重的错误(如自定义序列切片的部分),还对行文结构与章节衔接做了大量改动.原系列的单篇就不删除了,毕竟也是 ...

  9. Python进阶之面向对象编程

    面向对象编程——Object Oriented Programming,简称OOP,是一种程序设计思想.OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数. 面向过程的程序设计把计算机 ...

随机推荐

  1. jQuery 第一章 $()选择器

    jquery 是什么? jquery 其实就是一堆的js函数(js库),也是普通的js而已. 有点像我们封装一个函数,把他放到单独的js 文件,等待有需要的时候调用它. 那么使用它有啥好处呢? jqu ...

  2. mac下让iterm2记住远程ssh连接

    brew安装sshpass brew install http://git.io/sshpass.rb 在根目录下建立passowrd目录用来管理密码,vim testserver 输入明文密码,保存 ...

  3. 聊聊ReentrantLock实现原理

    ReentrantLock 是常用的锁,相对于Synchronized ,lock锁更人性化,阅读性更强 从LOCK切入 考虑下面的场景如果有A,B线程,同时去执行lock.lock(Lock loc ...

  4. 5. Idea集成Git

    5.1 引入本地安装的Git 5.2 本地库的初始化操作 5.3 本地库的基本操作 add与commit 控制台查看commit记录 查看Log 5.4 远程库的基本操作 远程库第一次pull到本地库 ...

  5. JDBC【1】-- 入门之增删改查

    目录 1.jdbc是什么 2.使用IDEA开发 2.1 创建数据库,数据表 2.2 使用IDEA创建项目 1.jdbc是什么 JDBC(Java DataBase Connectivity,java数 ...

  6. get、post、

    1.get请求 get请求会把参数放在url后面,中间用?隔开,也可以把参数放在请求body中,如果参数中有中文,http传的时候requests框架会将中文换成urlencode编码 2.get和p ...

  7. 【线程池】自己声明临时线程池一定要shutdown!

    场景: 某个定时任务需要多线程执行,执行时间较久且每天只跑一次,想单独拉出一个线程池和其他业务隔离开,交给spring会导致核心线程一直存在 浪费线程资源,因此想单独拉一个池子用完就丢,原本想的是,在 ...

  8. 再也不担心写出臃肿的Flink流处理程序啦,发现一款将Flink与Spring生态完美融合的脚手架工程-懒松鼠Flink-Boot

    目录 你可能面临如下苦恼: 接口缓存 重试机制 Bean校验 等等...... 它为流计算开发工程师解决了 有了它你的代码就像这样子: 仓库地址:懒松鼠Flink-Boot 1. 组织结构 2. 技术 ...

  9. PyQt(Python+Qt)学习随笔:QDockWidget停靠部件的setWidget和widget方法

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 QDockWidget对象由一个标题栏和内容区域组成.QDockWid ...

  10. 关于Python中中文文本文件使用二进制方式读取后的解码UnicodeDecodeError问题

    最近老猿在进行文件操作的验证测试,发现对于中文文本文件如果使用二进制方式打开,返回的类型是bytes,如果要转换成可读的字符串信息需要进行解码.可是老猿使用decode()或decode(" ...