当前位置: 首页 > 后端技术 > Python

如何正确使用yield?

时间:2023-03-25 19:45:36 Python

在Python开发中,yield关键字其实用的比较频繁,比如大集合的生成,代码结构的简化,协程和并发都会用到。但是你真的了解yield是如何工作的吗?在这篇文章中,我们就来看看yield的运行过程,以及哪些场景适合在开发中使用yield。生成器如果方法中包含yield关键字,则该函数是“生成器”。生成器实际上是一种特殊的迭代器,它可以像迭代器一样迭代输出方法中的每个元素。让我们看一个包含yield关键字的方法:#coding:utf8#Generatordefgen(n):foriinrange(n):yieldig=gen(5)#创建一个生成器print(g)#print(type(g))##迭代生成器中的数据foriing:print(i)#Output:#01234注意在这个例子中,当我们执行g=gen(5)时,实际上并没有执行gen中的代码。这个时候我们只是创建一个类型为generator的“generator对象”。那么,当我们执行foriing时,每执行一次循环,就会执行一次yield,返回yield之后的值。这个迭代过程是与迭代器最大的区别。也就是说,如果我们要输出5个元素,生成器创建的时候,这5个元素实际上还没有生成,那什么时候生成呢?仅当for循环遇到yield时,才会依次产生每个元素。另外,生成器除了像迭代器一样实现迭代数据,还包括其他方法:如果没有数据迭代,则抛出StopIterator异常,for循环结束[,value[,traceback]]):向生成器外部抛出异常generator.close():关闭生成器通过使用生成器的这些方法,我们可以完成很多有趣的功能。next我们先来看看generator的next方法。让我们看看下面的例子。#编码:utf8defgen(n):foriinrange(n):print('yieldbefore')yieldiprint('yieldafter')g=gen(3)#创建生成器print(g.__next__())#0print('----')print(g.__next__())#1print('----')print(g.__next__())#2print('----')print(g.__next__())#StopIteration#Output:#yieldbefore#0#----#yieldafter#yieldbefore#1#----#yieldafter#yieldbefore#2#----#yieldafter#Traceback(mostrecentcalllast):#File"gen.py",line16,in#print(g.__next__())#StopIteration#StopIteration在这个例子中,我们定义了gen方法,该方法包含了yield关键字。然后我们执行g=gen(3)来创建一个生成器,但是这次我们不是执行for来迭代它,而是多次调用g.__next__()来输出生成器中的元素。我们看到在执行g.__next__()的时候,代码会执行到yield,然后返回yield之后的值。如果你继续调用g.__next__(),注意,你会发现本次执行的开始位置,就是上次yield结束的地方,同时也保留了上次执行的上下文,继续往回迭代。这就是使用yield的作用。在迭代生成器时,每次执行都可以保留之前的状态,而不是像普通方法那样遇到return就返回结果,下次执行只能再次重复之前的过程。除了保存生成器的状态,我们还可以通过其他方式改变它的内部状态,就是下面要讲的send和throw方法。在上面send的例子中,我们只展示了yield之后还有value的情况。其实也可以使用j=yieldi的语法。我们看下面的代码:#coding:utf8defgen():i=1whileTrue:j=yieldii*=2ifj==-1:break如果我们此时执行下面的代码:foriingen():print(i)time.sleep(1)输出将是1248163264...循环继续,直到我们终止进程。这段代码之所以一直循环,是因为要等到分支j==-1跳出后才能执行。如果我们想让代码执行到这个地方,怎么办呢?这里使用了生成器的send方法。send方法可以将外部值传递给生成器,从而改变生成器的状态。代码可以这样写:g=gen()#创建生成器print(g.__next__())#1print(g.__next__())#2print(g.__next__())#4#sendput-1传进generator去了j=-1分支print(g.send(-1))#StopIteration迭代停止当我们执行g.send(-1)时,相当于把-1传入generator,而然后在yield前面赋值给j,此时j=-1,那么这个方法就会break掉,不会继续迭代。除了向生成器传递一个值,throw还可以传递一个异常,即调用throw方法:#coding:utf8defgen():try:yield1exceptValueError:yield'ValueError'finally:print('finally')g=gen()#创建生成器print(g.__next__())#1#将异常传递给生成器并返回ValueErrorprint(g.throw(ValueError))#Output:#1#ValueError#在finally示例中,生成器创建后,通过g.throw(ValueError)的方式向生成器传入异常,到达生成器异常处理的分支逻辑。关闭发电机的关闭方法也比较简单,就是手动关闭发电机,关闭的发电机不能再运行。>>>g=gen()>>>g.close()#关闭生成器>>>g.__next__()#无法迭代数据Traceback(mostrecentcalllast):File"",line1,中的StopIterationclose方法在开发中很少用到,了解一下即可。使用场景了解了yield和generator的使用方法,yield和generator一般用在哪些业务场景?下面我介绍几个例子,分别是大集合的生成、简化的代码结构、协程和并发。你可以参考这些使用场景来使用yield。大集合的生成如果要生成一个非常大的集合,如果使用list创建集合,这会导致在内存中分配很大的存储空间。例如,考虑以下内容:#coding:utf8defbig_list():result=[]foriinrange(10000000000):result.append(i)returnresult#一次在内存中生成一个大集合占用很大的记忆。foriinbig_list():print(i)这种场景下,我们使用生成器可以很好的解决这个问题。因为生成器只有在执行到yield时才会迭代数据,此时只申请需要返回元素的内存空间,所以代码可以这样写:#coding:utf8defbig_list():foriinrange(10000000000):yieldi#迭代时只顺序生成元素为i减少内存占用inbig_list():print(i)简化代码结构我们在开发中经常会遇到这样的场景,如果一个方法返回一个列表,但是这个list是只能由多个逻辑块组合生成,这会让我们的代码结构非常复杂:#coding:utf8defgen_list():#多个逻辑块组合生成一个listresult=[]foriinrange(10):result.append(i)forjinrange(5):result.append(j*j)forkin[100,200,300]:result.append(k)returnresultforitemingen_list():在print(item)的情况下,我们只能在每个逻辑块中使用append向列表中添加元素,代码比较冗长。这时候如果使用yield生成这个列表,代码会简洁很多:#coding:utf8defgen_list():#多个逻辑块使用yield生成一个listforiinrange(10):yieldiforjinrange(5):yieldj*jforkin[100,200,300]:yieldkforitemingen_list():print(i)使用yield后,不再需要定义类型变量list,并且只需要在每个逻辑块中直接yield返回元素即可,可以实现和前面例子一样的功能。我们可以看到使用yield的代码更加简洁,结构更加清晰。另一个好处是只在迭代元素时才分配内存空间,减少了内存资源的消耗。协程与并发还有一种场景yield用的比较多,那就是“协程与并发”。如果我们想提高程序的执行效率,我们通常会采用多进程、多线程的方式来编写程序代码。最常用的编程模型是“生产者-消费者”模型,即一个进程/线程生产数据,其他进程/线程消费数据。在开发多进程、多线程程序时,为了防止共享资源被篡改,我们通常需要加锁进行保护,这增加了编程的复杂度。在Python中,除了使用进程和线程,我们还可以使用“协程”来提高代码执行的效率。什么是协程?简单来说,由多个程序块组合执行的程序称为“协程”。在Python中使用“协程”,需要使用yield关键字来配合。也许很容易理解。我们使用yield来实现协程生产者和消费者的示例:#coding:utf8defconsumer():i=NonewhileTrue:#获取生产者发送的数据j=yieldiprint('consume%s'%j)defproducer(c):c.__next__()foriinrange(5):print('produce%s'%i)#发送数据给消费者c.send(i)c.close()c=consumer()producer(c)#Output:#produce0#consume0#produce1#consume1#produce2#consume2#produce3#consume3...这个程序的执行流程如下:c=consumer()Create一个生成器对象producer(c)开始执行,c.__next()__会启动生成器consumer,直到代码运行到j=yieldi,此时consumer第一次被执行,返回producer函数继续向下执行,直到c.send(i),这里使用generator的send方法向consumer发送数据。consumer函数被唤醒,从j=yieldi开始继续执行,收到producer的数据赋值给j,然后打印输出,直到yield再次执行,返回producer继续上面的过程循环,依次向cosnumer发送数据,直到循环结束,最后c.close()关闭消费者生成器,程序退出。在这个例子中我们发现程序在生产者和消费者两个函数之间来回切换,相互配合,完成了生产任务和消费任务的业务场景。最重要的是,整个程序是在单进程单线程下完成的。这个例子使用了上面提到的yield,生成器的__next__、send、close方法。如果难以理解,可以多看几遍这个例子,最好自己测试一下。当我们使用协程为生产者和消费者编写程序时,它的好处是:整个程序运行过程中没有锁,不需要考虑共享变量的保护,降低了编程复杂度。程序在功能之间来回切换。它是在用户态进行的,不像进程/线程会陷入内核态,减少了内核态上下文切换的消耗,执行效率更高。因此Python的yield和generator实现了协程的编程方式,为程序的并发执行提供了编程基础。Python中很多第三方库都是基于这个特性进行封装的,比如gevent、tornado等,大大提高了程序的运行效率。总结一下,在这篇文章中,我们主要讲了yield的使用和generator的各种特性。生成器是一种特殊的迭代器。除了迭代数据,它还可以在执行过程中保存方法中的状态。此外,它还提供了一种从外部改变内部状态,并将外部值传递给生成器的方法。利用yield和generator的特性,我们可以在开发中的大集成生成、代码结构简化、协程、并发等业务场景中使用。Python的yield也是实现协程和并发的基础。它提供了协程等用户态编程方式,提高了程序运行的效率。最近整理了数百G的Python学习资料,免费分享给大家!想上gong~hao“Python编程学习圈”,发“J”免费领取