Python中yield语句的工作原理、迭代协议和生成器表达式

       python中我们会看到在有些函数中有yield语句,其在函数中的作用和return语句类似,但是原理完全不一样。yield语句的主要特点是:当含有yield的被调用函数(也称之为生成器函数)在一个迭代环境中遍历迭代时,生成器函数并不是一次性的生成返回所有的值,而是每次迭代时返回迭代需要的值,并且被相应的迭代操作执行之后会被回收,然后继续下一次迭代。return语句一般是一次性返回被调用函数的结果,比如一个列表,然后在迭代环境中对这个列表进行迭代。对比return语句的一次性生成函数结果,yield语句是依次在每次迭代时仅生成返回列表中的一个元素,因此,yield语句相比而言会更少的占用内存,并且其底层过程也是用c语言封装的,因此在处理大数据时会更加的高效,甚至有时候在处理重量级数据时是必须的。

       那么yield语句的原理到底是怎么样的呢?一般的,含有yield语句的函数我们称之为生成器函数,是因为这种函数返回的对象是一个生成器对象,那什么又是生成器对象呢?这里我们不做深究,但要知道的是,这种对象是一种迭代器,迭代器遵守迭代协议。在说明yield的原理之前,我们需要先了解一下可迭代对象、迭代器、迭代工具和迭代协议。

       首先可迭代对象就是兼容iter()函数的对象,iter()是python的内置函数,可以把可迭代对象转化为迭代器,从而才可以进行迭代。实际上,python中的序列对象都是可迭代对象。迭代器就是含有__next__()方法的对象,比如文件对象本身就是迭代器对象,自带__next__()方法,不需要iter()的转换;其中对于没有__next__()的可迭代对象,iter()实际上就是通过给对象添加__next__()方法使其成为迭代器的。迭代工具就是对迭代器进行迭代的语句,比如for循环语句,迭代工具对于可迭代对象,会自动调用iter()方法将可迭代对象转为迭代器然后再进行迭代。迭代协议实际上就是迭代工具和迭代器之间的一个协议,内容就是在python中,一个迭代工具对一个迭代器进行迭代时,是通过调用迭代器的__next__()方法实现的,__next__()方法会依次返回迭代器对象中的被迭代元素,并把指针指向下一个元素,即下一次调用__next__()方法时,返回的就是下一个元素了。

       对于可迭代对象和迭代器,以及__next__()方法,我们可以利用列表对象来看一下。列表对象list是个可迭代对象,但不是迭代器,所以list对象没有__next__()方法,但是由于是可迭代对象,所以可以由iter()转为迭代器,然后就可以调用__next__()方法。

其中next()方法也是python的内置函数,next(a)其实就相当于调用对象a的__next__()方法。

       了解了以上概念和迭代协议后,下面讲yield语句的工作原理。对于生成器函数,当其被调用时,其返回的是一个生成器,之前说过,生成器是一个迭代器,因此其有__next__()方法,遵守迭代协议。我们可以把生成器对象看成是一个被调用函数的整个运行进程,这个进程以yield语句为分节点,一个进程会有多个yield分节点,每个节点会产生一个结果,而yield语句的作用就是向调用者返回这个结果并且把函数的进程状态挂起,并保留足够的变量以待这个进程下一次可以从这里识别并继续进行。因此,我们可以把这个进程中的多个yield语句看成传统的可迭代对象的元素,而__next__()方法会每次返回yield处生成的值并在下一次调用时继续从这里开始执行至下一个yield处。根据这个过程,我们就可以知道,其实生成器是需要结合迭代工具进行的,并且根据迭代协议进行多次的__next__()的调用,以使得生成器中的进程一直继续下去。区别于传统的迭代器对象,生成器对象需要的内存空间只是一个小进程需要的内存,当数据量很大时,一个相应大小的列表对象所需要的内存会远远超过这个小进程所需要的内存,因此,这就是为什么yield语句会节省内存的原因。

       下面我们通过简单的例子来看看yield语句在数据量较大时,传统方法比较占内存时体现出来的性能上的优势:

上图中,squr1是生成器函数,squr2是传统的函数,由于函数执行的操作比较简单,因此选取的数据量比较大,为1000000,以便体现出时间上的差距。可以看到,经过笔者多次的运行,后者的运行时间比前者运行多出的时间基本稳定在0.1s,这就是yield语句在性能上的优势。当函数相对复杂,对内存要求更高时,yield语句的性能优势会更加的明显。

       最后,讲一种可以快速生成一个生成器对象的方法——生成器表达式。生成器表达式和列表解析在形式上很像,唯一不同的地方就是列表解析最外面是方括号[],而生成器表达式最外面是圆括号()。如G=(i*i for i in range(7)),这个G就是一个生成器对象,且并不需要yield语句的参与。

       当数据量过大,执行函数相对复杂,对内存要求较高时,有时候用传统方法直接生成一个内存占用很高的对象甚至会出现电脑死机的情况,因此这时就不得不使用生成器函数。因此,了解这个原理和过程对于大数据工作者而言是有必要的。

猜你喜欢

转载自blog.csdn.net/S_o_l_o_n/article/details/81878032