前言

前一段时间和同事聊到Python技术知识,发现自己对生成器,迭代器傻傻分不清楚,于是乎查文档,找资料,有了此文。

通过本文大家可以了解到迭代器,生成器,生成器表达式,容器的定义以及关系。

一图胜千言

理解Python 生成器与迭代器_JAVA 系图(http://nvie.com/posts/iterators-vs-generators/)

先对上面的关系进行解释说明 

生成器包括生成器表达(generator expression)和生成器函数(generator function)。

生成器(generator)是迭代器(iterator),但是反过来不一定成立,同时生成器也是可迭代的。

迭代器(iterator)都是可迭代的(iterable),并且实现了next()/__next()__方法。

元组,列表,集合构成容器这些对象都是可迭代的。

接下来我们深入浅出的去了解迭代器,生成器是什么,如何使用。

可迭代对象

相信大家都知道迭代的含义,就是可以循环遍历。 那什么是可迭代对象?通俗的将就是可以使用for x in iterable_obj 或者while 循环遍历的对象,比如list,set,tuple,dict等对象。我们可以通过isinstance()方法来判断,参考例子:

In [30]: from collections import Iterable
In [31]: isinstance('python', Iterable)
Out[31]: True
In [32]: s=3           # s为数字,不可迭代
In [33]: isinstance(s, Iterable)
Out[33]: False
In [35]: l = [1,2,3,4] #列表可用for循环遍历,可迭代
In [35]: isinstance(l, Iterable)
Out[35]: True
In [37]: w=(1,2,3)     #元组可用for循环遍历,可迭代
In [38]: isinstance(w, Iterable)
Out[38]: True

从上面的检测来看 s=3 ,s是一个数值,不可迭代。其他的对象都是可以被循环访问的,即可迭代。

迭代器

迭代器是可以被next()函数调用并返回下一个值的对象,即Iterator。一个对象可迭代,是否就说明它是迭代器呢?我们继续使用isinstance 检测

In [37]: s=[1,2,3]      #列表可以用for循环遍历,可迭代
In [39]: from collections import Iterable,Iterator
In [40]: isinstance(s, Iterator)
Out[40]: False ##

从结果来看s是一个列表,可迭代的对象(iterable)不一定是迭代器(iterator)。为什么列表,集合,字典等对象是可迭代但是不是迭代器呢?

Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。

我们可以把iterator当做有序序列,但我们不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,只有在需要返回下一个数据时它才会计算,即(lazy evaluation)。构造有几百万个值的列表所占用的内存大于几十M,而迭代器对象占用几十个字节的空间。

如何构造迭代器呢?本文介绍两种方式:

  1. 为容器对象添加 __iter__() 和 __next__() 方法(Python 2.7 中是 next());__iter__() 返回迭代器对象本身 self,__next__() 则返回每次调用 next() 或迭代时的元素;

    class MyIterator(object):
         def __init__(self, max):
         self.max = max   # 上边界
         self.now = 0     # 当前迭代值,初始为0
         def __iter__(self):
             return self      # 返回迭代器本身
    
         def next(self):      # 迭代器类必须实现的方法next()
             while self.now < self.max:
                 self.now += 1
                 return self.now - 1  #返回当前迭代值
             raise StopIteration      #超出上边界,抛出异常
    
    In [51]: my=MyIterator(10)
    In [52]: isinstance(my,Iterable)
    Out[52]: True
    In [53]: isinstance(my,Iterator)
    Out[53]: True
  2. python内置函数 iter() 将可迭代对象转化为迭代器。 l 是一个列表,通过iter() 函数将列表对象转化为迭代器。

    In [45]: isinstance(l, Iterator)
    Out[45]: False
    In [46]: isinstance(iter(l), Iterator)
    Out[46]: True

生成器

  生成器是迭代器的一种,不过生成器不需要实现__iter__()和__next__(),只要使用yield关键字返回值。当一个生成器函数调用yield,生成器函数的"状态"会被冻结,所有的变量的值会被保留下来,下一行要执行的代码的位置也会被记录,直到再次调用next()。一旦next()再次被调用,生成器函数会从它上次离开的地方开始。如果永远不调用next(),yield保存的状态就被无视了。从下面的例子可以出来yield运行机制:

 In [79]: def gen():
    ...:     yield 1
    ...:     yield 2
    ...:     yield 3
    ...:

In [80]: g=gen()

In [81]: next(g)
Out[81]: 1

In [82]: next(g)
Out[82]: 2

In [83]: next(g)
Out[83]: 3

In [84]: next(g)
----------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-85-5f315c5de15b> in <module>()
----> 1 next(g)
StopIteration:

在python中生成器有两种:

  1. 生成器表达式 把列表生成式的[]中改为(),就创建一个generator

    In [90]: generator_expression = (x for x in range(5))
    
    In [91]: type(generator_expression)
    Out[91]: generator
    
    In [92]: next(generator_expression)
    Out[92]: 0
    
    In [93]: next(generator_expression)
    Out[93]: 1
  2. 生成器函数

    In [99]: def fib(max):
    ...:     n,a,b =0,0,1
    ...:     while n < max:
    ...:         yield b    ## 关键
    ...:         a,b =b,a+b
    ...:         n = n+1
    ...:
    
    In [102]: type(f)  
    Out[102]: generator
    
    In [103]: print f
    <generator object fib at 0x1097d1cd0>
    In [104]: f=fib(6)
    In [105]: next(f)
    Out[105]: 1
    In [106]: next(f)
    Out[106]: 1
    In [107]: next(f)
    Out[107]: 2
    In [108]: next(f)
    Out[108]: 3

    从102 步骤可以看出函数fib返回一个生成器对象,函数fib在每次调用next()的时候执行,遇到yield语句返回,再次被next()调用时候从上次的返回yield语句处继续执行,也就是用多少,取多少,不占内存。 

总结

  1. 函数如果定义返回的话,必须一次返回所有的结果,因为函数只能返回一次。生成器因为使用了 yield关键字,保存执行到yield的上下文,再次调用的时候可以直接继续执行下一步操作。

  2. 生成器是特殊的迭代器,只能执行一次。

  3. 生成器和迭代器两者都是可迭代对象。

  4. yield是一个类似return 的关键字,迭代一次遇到yield的时候就返回yield后面或者右面的值。而且下一次迭代的时候,从上一次迭代遇到的yield后面的代码开始执行