数据矿工学习-Python特性之迭代器与生成器

Python数据挖掘

Written by:

迭代器

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

凹脑图在线浏览地址:迭代器

仔细看完这份思维导图后,我们需要区分好两个概念可迭代对象(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读取文件代码:

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

才学疏浅,欢迎评论指导 

参考资料:

廖雪峰老师的Python yield 使用浅析

Billy.J.Hee的技术博客

(译)Python关键字yield的解释(stackoverflow)

发表评论

电子邮件地址不会被公开。 必填项已用*标注