转载

python迭代器与生成器小结

源自 我的博客

例子

老规矩,先上一个代码:

def add(s, x):     return s + x  def gen():     for  i in range(4):         yield i  base = gen() for n in [1, 10]:     base = (add(i + n) for i in base)  print list(base)

这个东西输出可以脑补一下, 结果是 [20,21,22,23] , 而不是 [10, 11, 12, 13] 。 当时纠结了半天,一直没搞懂,后来 齐师 稍微指点了一下, 突然想明白了--真够笨的,唉。。好了--正好趁机会稍微小结一下python里面的生成器。

迭代器(iterator)

要说生成器,必须首先说迭代器

区分iterable,iterator与itertion

讲到迭代器,就需要区别几个概念: iterable , iterator , itertion , 看着都差不多,其实不然。下面区分一下。

  • itertion : 就是 迭代 ,一个接一个(one after another),是一个通用的概念,比如一个循环遍历某个数组。

  • iterable : 这个是 可迭代对象 ,属于python的名词,范围也很广,可重复迭代,满足如下其中之一的都是 iterable :

    • 可以 for 循环: for i in iterable

    • 可以按 index 索引的对象,也就是定义了 __getitem__ 方法,比如 list,str ;

    • 定义了 __iter__ 方法。可以随意返回。

    • 可以调用 iter(obj) 的对象,并且返回一个 iterator

  • iterator : 迭代器对象 ,也属于python的名词,只能迭代一次。需要满足如下的 迭代器协议

    • 定义了 __iter__ 方法,但是必须 返回自身

    • 定义了 next 方法,在python3.x是 __next__用来返回下一个值 ,并且当没有数据了,抛出 StopIteration

    • 可以保持当前的状态

首先 str和listiterable 但不是 iterator :

In [3]: s = 'hi'  In [4]: s.__getitem__ Out[4]: <method-wrapper '__getitem__' of str object at 0x7f9457eed580>  In [5]: s.next # 没有next方法 --------------------------------------------------------------------------- AttributeError                            Traceback (most recent call last) <ipython-input-5-136d3c11be25> in <module>() ----> 1 s.next  AttributeError: 'str' object has no attribute 'next'  In [6]: l = [1,2] # 同理  In [7]: l.__iter__ Out[7]: <method-wrapper '__iter__' of list object at 0x7f945328c320>  In [8]: l.next --------------------------------------------------------------------------- AttributeError                            Traceback (most recent call last) <ipython-input-8-c6f8fb94c4cd> in <module>() ----> 1 l.next  AttributeError: 'list' object has no attribute 'next' In [9]: iter(s) is s #iter() 没有返回本身 Out[9]: False In [10]: iter(l) is l #同理 Out[10]: False

但是对于 iterator 则不一样如下, 另外 iterable 可以支持多次迭代,而 iterator 在多次 next 之后,再次调用就会抛异常,只可以迭代一次。

In [13]: si = iter(s)  In [14]: si Out[14]: <iterator at 0x7f9453279dd0>  In [15]: si.__iter__ # 有__iter__ Out[15]: <method-wrapper '__iter__' of iterator object at 0x7f9453279dd0>  In [16]: si.next #拥有next Out[16]: <method-wrapper 'next' of iterator object at 0x7f9453279dd0>  In [20]: si.__iter__() is si #__iter__返回自己 Out[20]: True

这样,由这几个例子可以解释清楚这几个概念的区别。

自定义iterator 与数据分离

说到这里,迭代器对象基本出来了。下面大致说一下,如何让自定义的类的对象成为迭代器对象,其实就是定义 __iter__next 方法:

In [1]: %paste class DataIter(object):      def __init__(self, *args):         self.data = list(args)         self.ind = 0      def __iter__(self): #返回自身         return self      def next(self): # 返回数据         if self.ind == len(self.data):             raise StopIteration         else:             data = self.data[self.ind]             self.ind += 1             return data ## -- End pasted text --  In [9]: d  = DataIter(1,2)  In [10]: for x in d: # 开始迭代    ....:     print x    ....: 1 2  In [13]: d.next() # 只能迭代一次,再次使用则会抛异常 --------------------------------------------------------------------------- StopIteration                             Traceback (most recent call last) ----> 1 d.next() <ipython-input-1-c44abc1904d8> in next(self)      10     def next(self):      11         if self.ind == len(self.data): ---> 12             raise StopIteration      13         else:      14             data = self.data[self.ind] 

next 函数中只能向前取数据,一次取一个可以看出来,不过不能重复取数据,那这个可不可以解决呢?

我们知道 iterator 只能迭代一次,但是 iterable 对象则没有这个限制,因此我们可以把 iterator 从数据中分离出来,分别定义一个 iterableiterator 如下:

class Data(object):   # 只是iterable:可迭代对象而不iterator:迭代器      def __init__(self, *args):         self.data = list(args)      def __iter__(self):  # 并没有返回自身         return DataIterator(self)   class DataIterator(object):  # iterator: 迭代器      def __init__(self, data):         self.data = data.data         self.ind = 0      def __iter__(self):         return self      def next(self):         if self.ind == len(self.data):             raise StopIteration         else:             data = self.data[self.ind]             self.ind += 1             return data  if __name__ == '__main__':     d = Data(1, 2, 3)     for x in d:         print x,     for x in d:         print x, 

输出就是:

1,2,3 1,2,3

可以看出来数据可以复用,因为每次都返回一个 DataIterator ,但是数据却可以这样使用,这种实现方式很常见,比如 xrange 的实现便是这种数据与迭代分离的形式,但是很节省内存,如下:

In [8]: sys.getsizeof(range(1000000)) Out[8]: 8000072  In [9]: sys.getsizeof(xrange(1000000)) Out[9]: 40

另外有个小tips, 就是为什么可以使用for 迭代迭代器对象,原因就是 for 替我们做了 next 的活,以及接收 StopIteration 的处理。

迭代器大概就记录到这里了,下面开始一个特殊的更加优雅的迭代器: 生成器

生成器(generator)

首先需要明确的就是生成器也是 iterator 生成器,因为它遵循了迭代器协议.

两种创建方式

包含 yield 的函数

生成器函数跟普通函数只有一点不一样,就是把 return 换成 yield ,其中 yield 是一个语法糖,内部实现了迭代器协议,同时保持状态可以挂起。如下:

def gen():     print 'begin: generator'     i = 0     while True:         print 'before return ', i         yield i         i += 1         print 'after return ', i  a  = gen()  In [10]: a #只是返回一个对象 Out[10]: <generator object gen at 0x7f40c33adfa0>  In [11]: a.next() #开始执行 begin: generator before return  0 Out[11]: 0  In [12]: a.next() after return  1 before return  1 Out[12]: 1

首先看到 while True 不必惊慌,它只会一个一个的执行~

看结果可以看出一点东西:

  • 调用 gen() 并没有真实执行函数,而是只是返回了一个生成器对象

  • 执行第一次 a.next() 时,才真正执行函数,执行到 yield 一个返回值,然后就会挂起,保持当前的名字空间等状态。然后等待下一次的调用,从 yield 的下一行继续执行。

还有一种情况也会执行生成器函数,就是当检索生成器的元素时,如 list(generator) , 说白了就是当需要数据的时候,才会执行。

In [15]: def func():    ....:     print 'begin'    ....:     for i in range(4):    ....:         yield i  In [16]: a = func()  In [17]: list(a) #检索数据,开始执行 begin Out[17]: [0, 1, 2, 3] 

yield 还有其他高级应用,后面再慢慢学习。

生成器表达式

列表生成器十分方便:如下,求10以内的奇数:

[i for i in range(10) if i % 2]

同样在 python 2.4 也引入了 生成器表达式 ,而且形式非常类似,就是把 [] 换成了 () .

In [18]: a = ( i for i in range(4))  In [19]: a Out[19]: <generator object <genexpr> at 0x7f40c2cfe410>  In [20]: a.next() Out[20]: 0

可以看出生成器表达式创建了一个生成器,而且生有个特点就是 惰性计算 , 只有在被检索时候,才会被赋值。

之前有篇文章: python 默认参数问题及一个应用 ,最后有一个例子:

def multipliers():     return (lambda x : i * x for i in range(4))  #修改成生成器 print [m(2) for m in multipliers()]

这个就是说,只有在执行 m(2) 的时候,生成器表达式里面的 for 才会开始从0循环,然后接着才是 i * x ,因此不存在那篇文章中的问题。

惰性计算这个特点很有用,上述就是一个应用, 2gua 这样说的:

性计算想像成水龙头,需要的时候打开,接完水了关掉,这时候数据流就暂停了,再需要的时候再打开水龙头,这时候数据仍是接着输出,不需要从头开始循环

其实本质跟迭代器差不多,不一次性把数据都那过来,需要的时候,才拿。

回到例子

看到这里,开始的例子应该大概可以有点清晰了,核心语句就是:

for n in [1, 10]:     base = (add(i + n) for i in base)

在执行 list(base) 的时候,开始检索,然后生成器开始运算了。关键是,这个循环次数是2,也就是说,有两次生成器表达式的过程。必须牢牢把握住这一点。

生成器返回去开始运算, n = 10 而不是1没问题吧,这个在上面提到的文章中已经提到了,就是add(i+n)绑定的是 n 这个变量,而不是它当时的数值。

然后首先是第一次生成器表达式的执行过程: base = (10 + 0, 10 + 1, 10 + 2, 10 +3) ,这是第一次循环的结果(形象表示,其实已经计算出来了(10,11,12,3)),然后第二次, base = (10 + 10, 11 + 10, 12 + 10, 13 + 10) ,终于得到结果了 [20, 21, 22, 23] .

具体执行过程可以在 pythontutor 上手动看看执行过程。

小结

概括

主要介绍了大概这样几点:

  • iterable , iteratoritertion 的概念

  • 迭代器协议

    • 自定义可迭代对象与迭代器分离,保证数据复用

  • 生成器: 特殊的迭代器,内部实现了迭代器协议

其实这一块, 那几个概念搞清楚, ,这个很关键, 搞懂了后面就水到渠成了。而且对之前的知识也有很多加深。

比如常见 list 就是 iteratoriteable 分离实现的,本身是可迭代对象,但不是迭代器, 类似与 xrange ,但是又不同。

越来越明白,看源码的重要性了。 有地方写的不合适的, 请指正。

参考

  • http://www.shutupandship.com/2012/01/understanding-python-iterables-and.html

  • http://www.learningpython.com/2009/02/23/iterators-iterables-and-generators-oh-my/

  • http://stackoverflow.com/questions/9884132/what-exactly-are-pythons-iterator-iterable-and-iteration-protocols

  • http://python.jobbole.com/81881/

原文  https://segmentfault.com/a/1190000004554823
正文到此结束
Loading...