Python-yield关键字详解
前言
yield
这个关键字很早的时候就了解过,但一直都只了解其基本使用,即转变函数为生成器的使用,节省大型迭代时的内存空间,但其实yield
在python的很多特性中都起着重要的作用
这篇文章就详细展开一下yield
关键字
需要了解的几个词
容器(container)
:python中容器指一个用来存储多个元素的数据结构,常见的list,tuple,dict,set,string都是容器可迭代对象(iterable)
:在python中能被迭代获取的对象,iterable的对象一定实现了__iter__()
方法迭代器(iterator)
:迭代器实现了__iter__()
和__next__()
方法,是一个带状态的对象,迭代器内部持有一个状态,该状态用于记录**当前迭代所在位置,**以便于下次迭代的时候获取正确的元素,迭代器可以通过next()方法来迭代获取下一个值生成器(generator)
:生成器是一种特殊的迭代器,特殊在我们可以通过send()方法向生成器中传入数据,而迭代器只能将数据输出协程(coroutine)
:与线程很相似,不同之处在于多协程是同一个线程来执行的,这样就省去了线程切换的时间,而且不需要多线程的锁机制了,执行效率高很多
可迭代对象,迭代器与生成器的关系
简单来说可以用以下的韦恩图表示:
从设计角度讲,容器是我们最常见最常用的数据结构,它们都是可迭代对象,而另一方面我们也可以在自定义类中实现__iter__()
方法将该类的对象变成可迭代的
但这样的迭代有一个缺点,当一次迭代的内容过多时,会占用大量内存;这时迭代器就出现了,其实现了__next__()
方法,在每个单次迭代中记录位置,每次返回一个值来进行完整的迭代,实现一种惰性的获取数据的方式;这样就不需要一次性把所有内容加载到内存,而是在需要的时候返回单个结果
生成器是一种特殊的迭代器,可以通过生成器函数和生成器表达式来创建生成器,其自带了__iter__()
和__next__()
方法,通过创建生成器来创建迭代器可以让我们更专注于业务逻辑的实现;此外其还实现了send()方法,可以往生成器函数中传值,赋给yield关键字的左值
生成器中的yield
在一个函数中使用yield关键字,这个函数就变成了生成器函数,看一个经典的输出斐波那契数列实现:
1 | def fib(max): |
非常优雅地就可以实现斐波那契数列的输出,并且在进行for循环时每次只占用一个数的内存
但使用for循环进行迭代我们无法获取到生成器函数的返回值(生成器结束迭代时会抛出StopIteration
异常,但这个异常被for循环捕获并pass了);想要获取返回值我们需要抛弃for循环,自己来捕获异常:
1 | g = fib(6) |
协程中的yield
Python对协程的支持是通过generator实现的,在一般的generator使用中,我们不但可以通过for
循环来迭代,还可以不断调用next()
函数获取由yield
语句返回的下一个值;但在生成器中我们还可以通过send向生成器函数中传值;到这里我们就可以拿出经典的生产者-消费者模型来看看了:
1 | def consumer(): |
注意到consumer
函数是一个generator
,把一个consumer
传入produce
后:
- 首先调用
c.send(None)
启动生成器; - 然后,一旦生产了东西,通过
c.send(n)
切换到consumer
执行; consumer
通过yield
拿到消息,处理,又通过yield
把结果传回;produce
拿到consumer
处理的结果,继续生产下一条消息;produce
决定不生产了,通过c.close()
关闭consumer
,整个过程结束。
整个流程无锁,由一个线程执行,produce
和consumer
协作完成任务,所以称为“协程”,而非线程的抢占式多任务
如果没有协程,我们在写一个并发业务时会遇到以下问题:
- 使用最常规的同步编程要实现异步并发效果并不理想,或者难度极高
- 由于GIL锁的存在,多线程的运行需要频繁的加锁解锁,切换线程,这极大地降低了并发性能
而有了协程,我们就可以非常优雅高性能地实现一些高IO的并发任务了
yield from
yield from
是Python 3.3
中才出现的语法,所以这个特性在Python 2
中是没有的,yield from
语法可以让我们方便地调用另一个generator
yield from
后面需要加的是可迭代对象,它可以是普通的可迭代对象,也可以是迭代器或生成器(不记得关系了的可以往上翻一翻)
应用一:拼接可迭代对象
我们可以用一个使用yield
和一个使用yield from
的例子来对比看
使用yield
1 | # 字符串 |
使用yield from
1 | # 字符串 |
由上面两种方式的对比,可以看出,yield from后面加上可迭代对象,他可以把可迭代对象里的每个元素一个一个的yield出来,对比yield来说代码更加简洁,结构更加清晰
应用二:生成器的嵌套
上面的只是yield from
很简单的一个应用,它真正的作用并不在此;当 yield from
后面加上一个生成器后,就实现了生成器的嵌套
当然实现生成器的嵌套,并不是一定必须要使用yield from
,而是使用yield from
可以让我们避免让我们自己处理各种料想不到的异常,而让我们专注于业务代码的实现
如果自己用yield
去实现,那只会加大代码的编写难度,降低开发效率,降低代码的可读性。既然Python已经想得这么周到,我们当然要好好利用起来
讲解它之前,首先要知道这个几个概念
预激活
:通过next()
方法或send(None)
方法使生成器第一次停在yield
关键字处,状态由GEN_CREATED
变为GEN_SUSPENDED
调用方
:调用委派生成器的客户端(调用方)代码委托生成器
:包含yield from
表达式的生成器函数子生成器
:yield from
后面加的生成器函数
直接看代码:
1 | from inspect import getgeneratorstate |
输出:
1 | GEN_CREATED |
上面是一个实时计算平均值的实现,流程这里就不展开讲了,仔细看的都看得清楚,讲一下值得注意的几个点(结合注释):
inspect.getgeneratorstate
方法可以获取生成器的状态yield from
会对子生成器进行预激活- 委托生成器只起一个桥梁作用,它建立的是一个
双向通道
,它并没有权利也没有办法,对子生成器yield回来的内容做拦截 yield from
会帮我们处理子生成器的所有异常(不只是最常见的StopIteration
)
总结
yield
关键字在Python中可以说很重要了,很多地方的实现都是使用它,尤其在并发编程中,协程的实现也让我们的开发优雅简洁了不少