大数据分析挖掘技术:Python特性之迭代器与生成器

Python的迭代器与生成器的相关概念是同学们容易搞混的知识点,下面就python的迭代器与生成器的概念以及相关使用作下简要介绍
 

迭代器


要知道生成器是啥,首先得先了解下迭代器是什么,概念的部分还是用我最喜欢的老套路思维导图来表示:

仔细看完这份思维导图后,我们需要区分好两个概念可迭代对象(iterable)迭代器(iterator)

num = [0,1,2,3,4] 
for i in num: 
    print(i) 

这里的列表num符合上面的条件之一:可以for循环,所以列表num可以称之为可迭代对象,那num可以说是迭代器吗?我们可以用isinstance方法来验证下:

In [2]: from collections import Iterator 
In [3]: isinstance(num,Iterator) 
Out[3]: False 

答案是False,因为列表num并不符合迭代器协议,简单点来讲,列表num里面并没有__iter__方法__next__方法。下面我们按照迭代器协议要求自己来构造一个迭代器:

class numIter:         #迭代器 
    def __init__(self,n): 
        self.n = n 
     
    def __iter__(self): 
        self.x = -1 
        return self 

    def __next__(self):         #Python 3.x版本     Python 2.x版本是next() 
        self.x += 1 
        if self.x < self.n: 
            return self.x 
        else: 
            raise StopIteration 

for i in numIter(5): 
    print(i) 

numIter里面包含了_iter_方法和_next_方法,符合了迭代器协议,numIter是不是迭代器呢?下面我们继续使用isinstance方法来验证:

In [5]: isinstance(numIter,Iterator) 
Out[5]: False 

False!?!这里需要注意的是,numIter只是个类定义,本身是不会迭代的,而numIter(5)这个类的实例才可以进行迭代:

In [7]: isinstance(numIter(5),Iterator) 
Out[7]: True 

生成器


生成器也是一种特殊的迭代器,概念部分继续惯例思维导图贴上:

看完了思维导图,我们继续回到上面的那句话生成器也是一种特殊的迭代器,从上面的生成器运行流程中我们不难发现两个身影返回自身对象next方法返回迭代值,这不就是我们上面迭代器讲的迭代器协议(__iter__方法和__next__方法)吗?我们还是来用isinstance来验证一下

先用生成器表达式来生成一个表达器:

In [13]: num = (i for i in range(5)) #注意这里使用的是()不是[] 
In [14]: for i in num: 
...: print(i) 

isinstance验证是否为迭代器:

In [15]: isinstance(num,Iterator) 
Out[15]: True 

答案为true,证明了生成器也是一种迭代器,那为什么要说生成器是一种特殊的迭代器呢?这时我们就得来看另一种生成器的生成方法-生成器函数

def numGen(n):         #生成器 
    x = 0 
    while x < n: 
        yield x 
        x += 1 

非常简短的几行代码,关键就在于yield这个关键字,一般来说如果我们的函数中出现了yield关键字,调用该函数时就会返回成一个生成器,为了更清楚地理解yield这个关键字的作用,我们还是用代码来说话:

In [19]: num = numGen(3) #得到一个生成器对象 
In [20]: print(num.__next__()) #执行next方法 
0 

In [21]: print(num.__next__()) 
1 

In [22]: print(num.__next__()) 
2 

In [23]: print(num.__next__()) 
--------------------------------------------------------------------------- 
StopIteration Traceback (most recent call last) 

首先我们运行第一行代码

num = numGen(3) #得到一个生成器对象 

得到一个生成器对象,很容易理解的一行代码,但当我们与普通的return方法进行对比时,我们就会发现一个有趣的现象:

def numGen(n):         #生成器 
    x = 0 
    print("生成器执行中") 
    while x < n: 
        yield x 
        x += 1 

def numGen1(n): 
    x = 0 
    print("普通方法执行中") 
    while x < n: 
        x += 1 
        return x 
num = numGen(3) 
num1 = numGen1(3) 
输出:普通方法执行中 

从这个例子我们就可以看出,当我们生成一个生成器对象时,生成器函数内部的代码并不会马上执行,而普通return函数生成对象时即开始运行内部代码,那生成器函数的代码时什么时候开始执行的呢?别急我们来运行下一行代码:

In [25]: print(num.__next__() 
生成器执行中 
0 

得出答案,生成器函数的内部代码是在执行next()方法后才开始执行的,新的问题又出现了代码是执行到关键字yield就暂停还是整段代码运行完才暂停,这里我们将上面的例子再次改装(然而我第一次在ipython运行时遇到了一个“bug”,代码如下):

In [26]: def numGen(n):         #生成器 
    ...:     x = 0 
    ...:     print("生成器执行yield前") 
    ...:     while x < n: 
    ...:         yield x 
    ...:         print("生成器执行yield后") 
    ...:         x += 1 
    ...: 

In [27]: print(num.__next__()) 
1  

这其实不是bug,这是生成器的·一个特性:只可以读取一次,所以这里得再重新运行一次:

In [1]: def numGen(n):         #生成器 
   ...:     x = 0 
   ...:     print("生成器执行yield前") 
   ...:     while x < n: 
   ...:         yield x 
   ...:         print("生成器执行yield后") 
   ...:         x += 1 
   ...: 

In [2]: num = numGen(3) 

In [3]: print(num.__next__()) 
生成器执行yield前 
0 

In [4]: print(num.__next__()) 
生成器执行yield后 
1 


In [5]: print(num.__next__()) 
生成器执行yield后 
2 


In [6]: print(num.__next__()) 
生成器执行yield后 
--------------------------------------------------------------------------- 
StopIteration                             Traceback (most recent call last) 

有了这个例子,我们就能很好地理解关键字yield的作用了,当代码运行到关键字yield时,执行中断并返回当前的迭代值,除此之外当前的上下文环境也会被记录下来,简单点讲就是执行中断的位置数据都被保存起来。再次使用 next() 的时候,从原来中断的地方继续执行,直至遇到 yield,如果没有 yield,则抛出StopIteration 异常。

了解了生成器的运行机制,最后我们再来了解下生成器其余的三种方法
 

send()方法


In [3]: def numGen(n):         #生成器 
   ...:     x = 0 
   ...:     while x < n: 
   ...:         y = yield x 
   ...:         print(y) 
   ...:         x += 1 
   ...: num = numGen(3) 
   ...: print(num.__next__()) 
   ...: print(num.send(999)) 
   ...: print(num.__next__()) 
   ...: print(num.__next__()) 
   ...: 
0 
999 
1 
None 
2 
None 
--------------------------------------------------------------------------- 
StopIteration                             Traceback (most recent call last) 

下面来说下运行流程:

首先调用next()方法,让生成器内部代码执行到关键字yield处,返回0;

接着调用send(999)方法,将值999传到代码执行中断的地方,也就是关键字yield处,将999赋值给y,输出y,执行x+=1,执行到关键字yield处,返回1;

继续调用next()方法,无值赋给y,y=None,输出y,执行x+=1,执行到关键字yield处,返回2;

继续调用next()方法,无值赋给y,y=None,输出y,执行x+=1,x=n跳出while循环,找不到关键字yield,抛出StopIteration 异常;
 

throw()方法


In [4]: def numGen(n):         #生成器 
   ...:     try: 
   ...:         x = 0 
   ...:         while x < n: 
   ...:             yield x 
   ...:             x += 1 
   ...:     except ValueError: 
   ...:         yield 'Error' 
   ...:     finally: 
   ...:         print('Finally') 
   ...:   
   ...: num = numGen(3) 
   ...: print(num.__next__()) 
   ...: print(num.throw(ValueError)) 
   ...: print(num.__next__()) 
   ...: 
0 
Error 
Finally 
--------------------------------------------------------------------------- 
StopIteration                             Traceback (most recent call last) 

可以看出当我们向生成器抛去ValueError错误时,整个生成器就执行finally,最后抛出StopIteration 异常;
 

close()方法


In [5]: def numGen(n):         #生成器 
   ...:     x = 0 
   ...:     while x < n: 
   ...:         yield x 
   ...:         x += 1 
   ...:   
   ...: num = numGen(3) 
   ...: print(num.__next__()) 
   ...: num.close() 
   ...: print(num.__next__()) 
   ...: 
0 
--------------------------------------------------------------------------- 
StopIteration                             Traceback (most recent call last) 

当我们运行close()方法时,整个生成器就终止了,再执行next()方法,就抛出StopIteration 异常;

最后,学了这么多,生成器到底有什么过人之处:

1)由于生成器这种“走停走停”策略,使得生成器可以逐步生成序列,不用像list一样初始化时就要开辟所有的空间,所以当你一次只需对一个数进行处理时,使用生成器是一个不错的选择。

2)运用好生成器的四种方法next()throw()send()close()还有生成器的关键字yield的特性,是可以实现伪并发操作的,Python虽然支持多线程,可由于GIL(全局解释锁)的存在,使得同一时刻只能有一条线程运行,并没有办法并行操作,所以Python的多线程实际上就是鸡肋。

3)我们在读取文件时,如果直接对文件对象调用 read() 方法,会导致不可预测的内存占用。好的方法是利用固定长度的缓冲区来不断读取文件内容。通过 yield,我们不再需要编写读文件的迭代类,就可以轻松实现文件读取:

下面贴上廖雪峰老师的yield读取文件代码(大数据技术学习交流群:805017805,不定期分享学习干货):

def read_file(fpath): 
   BLOCK_SIZE = 1024  
   with open(fpath, 'rb') as f:  
       while True:  
           block = f.read(BLOCK_SIZE)  
           if block:  
               yield block  
           else:  
               return 

猜你喜欢

转载自blog.csdn.net/jiamigu/article/details/81237023
今日推荐