python迭代器与生成器详解
例子
老规矩,先上一个代码:
defadd(s,x): returns+x defgen(): foriinrange(4): yieldi base=gen() fornin[1,10]: base=(add(i,n)foriinbase) printlist(base)
这个东西输出可以脑补一下,结果是[20,21,22,23],而不是[10,11,12,13]。当时纠结了半天,一直没搞懂,后来齐老师稍微指点了一下,突然想明白了--真够笨的,唉。。好了--正好趁机会稍微小结一下python里面的生成器。
迭代器(iterator)
要说生成器,必须首先说迭代器
区分iterable,iterator与itertion
讲到迭代器,就需要区别几个概念:iterable,iterator,itertion,看着都差不多,其实不然。下面区分一下。
itertion:就是迭代,一个接一个(oneafteranother),是一个通用的概念,比如一个循环遍历某个数组。
iterable:这个是可迭代对象,属于python的名词,范围也很广,可重复迭代,满足如下其中之一的都是iterable:
可以for循环:foriiniterable
可以按index索引的对象,也就是定义了__getitem__方法,比如list,str;
定义了__iter__方法。可以随意返回。
可以调用iter(obj)的对象,并且返回一个iterator
iterator:迭代器对象,也属于python的名词,只能迭代一次。需要满足如下的迭代器协议
定义了__iter__方法,但是必须返回自身
定义了next方法,在python3.x是__next__。用来返回下一个值,并且当没有数据了,抛出StopIteration
可以保持当前的状态
首先str和list是iterable但不是iterator:
In[3]:s='hi' In[4]:s.__getitem__ Out[4]:<method-wrapper'__getitem__'ofstrobjectat0x7f9457eed580> In[5]:s.next#没有next方法 --------------------------------------------------------------------------- AttributeErrorTraceback(mostrecentcalllast) <ipython-input-5-136d3c11be25>in<module>() ---->1s.next AttributeError:'str'objecthasnoattribute'next' In[6]:l=[1,2]#同理 In[7]:l.__iter__ Out[7]:<method-wrapper'__iter__'oflistobjectat0x7f945328c320> In[8]:l.next --------------------------------------------------------------------------- AttributeErrorTraceback(mostrecentcalllast) <ipython-input-8-c6f8fb94c4cd>in<module>() ---->1l.next AttributeError:'list'objecthasnoattribute'next' In[9]:iter(s)iss#iter()没有返回本身 Out[9]:False In[10]:iter(l)isl#同理 Out[10]:False
但是对于iterator则不一样如下,另外iterable可以支持多次迭代,而iterator在多次next之后,再次调用就会抛异常,只可以迭代一次。
In[13]:si=iter(s) In[14]:si Out[14]:<iteratorat0x7f9453279dd0> In[15]:si.__iter__#有__iter__ Out[15]:<method-wrapper'__iter__'ofiteratorobjectat0x7f9453279dd0> In[16]:si.next#拥有next Out[16]:<method-wrapper'next'ofiteratorobjectat0x7f9453279dd0> In[20]:si.__iter__()issi#__iter__返回自己 Out[20]:True
这样,由这几个例子可以解释清楚这几个概念的区别。
自定义iterator与数据分离
说到这里,迭代器对象基本出来了。下面大致说一下,如何让自定义的类的对象成为迭代器对象,其实就是定义__iter__和next方法:
In[1]:%paste classDataIter(object): def__init__(self,*args): self.data=list(args) self.ind=0 def__iter__(self):#返回自身 returnself defnext(self):#返回数据 ifself.ind==len(self.data): raiseStopIteration else: data=self.data[self.ind] self.ind+=1 returndata ##--Endpastedtext-- In[9]:d=DataIter(1,2) In[10]:forxind:#开始迭代 ....:printx ....: 1 2 In[13]:d.next()#只能迭代一次,再次使用则会抛异常 --------------------------------------------------------------------------- StopIterationTraceback(mostrecentcalllast) ---->1d.next() <ipython-input-1-c44abc1904d8>innext(self) 10defnext(self): 11ifself.ind==len(self.data): --->12raiseStopIteration 13else: 14data=self.data[self.ind]
从next函数中只能向前取数据,一次取一个可以看出来,不过不能重复取数据,那这个可不可以解决呢?
我们知道iterator只能迭代一次,但是iterable对象则没有这个限制,因此我们可以把iterator从数据中分离出来,分别定义一个iterable与iterator如下:
classData(object):#只是iterable:可迭代对象而不iterator:迭代器 def__init__(self,*args): self.data=list(args) def__iter__(self):#并没有返回自身 returnDataIterator(self) classDataIterator(object):#iterator:迭代器 def__init__(self,data): self.data=data.data self.ind=0 def__iter__(self): returnself defnext(self): ifself.ind==len(self.data): raiseStopIteration else: data=self.data[self.ind] self.ind+=1 returndata if__name__=='__main__': d=Data(1,2,3) forxind: printx, forxind: printx,
输出就是:
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是一个语法糖,内部实现了迭代器协议,同时保持状态可以挂起。如下:
defgen(): print'begin:generator' i=0 whileTrue: print'beforereturn',i yieldi i+=1 print'afterreturn',i a=gen() In[10]:a#只是返回一个对象 Out[10]:<generatorobjectgenat0x7f40c33adfa0> In[11]:a.next()#开始执行 begin:generator beforereturn0 Out[11]:0 In[12]:a.next() afterreturn1 beforereturn1 Out[12]:1
首先看到whileTrue不必惊慌,它只会一个一个的执行~
看结果可以看出一点东西:
调用gen()并没有真实执行函数,而是只是返回了一个生成器对象
执行第一次a.next()时,才真正执行函数,执行到yield一个返回值,然后就会挂起,保持当前的名字空间等状态。然后等待下一次的调用,从yield的下一行继续执行。
还有一种情况也会执行生成器函数,就是当检索生成器的元素时,如list(generator),说白了就是当需要数据的时候,才会执行。
In[15]:deffunc(): ....:print'begin' ....:foriinrange(4): ....:yieldi In[16]:a=func() In[17]:list(a)#检索数据,开始执行 begin Out[17]:[0,1,2,3]
yield还有其他高级应用,后面再慢慢学习。
生成器表达式
列表生成器十分方便:如下,求10以内的奇数:
[i foriinrange(10)ifi%2]
同样在python2.4也引入了生成器表达式,而且形式非常类似,就是把[]换成了().
In[18]:a=(iforiinrange(4)) In[19]:a Out[19]:<generatorobject<genexpr>at0x7f40c2cfe410> In[20]:a.next() Out[20]:0
可以看出生成器表达式创建了一个生成器,而且生有个特点就是惰性计算,只有在被检索时候,才会被赋值。
之前有篇文章:python默认参数问题及一个应用,最后有一个例子:
defmultipliers(): return(lambdax:i*xforiinrange(4))#修改成生成器 print[m(2)forminmultipliers()]
这个就是说,只有在执行m(2)的时候,生成器表达式里面的for才会开始从0循环,然后接着才是i*x,因此不存在那篇文章中的问题。
惰性计算这个特点很有用,上述就是一个应用,2gua这样说的:
性计算想像成水龙头,需要的时候打开,接完水了关掉,这时候数据流就暂停了,再需要的时候再打开水龙头,这时候数据仍是接着输出,不需要从头开始循环
其实本质跟迭代器差不多,不一次性把数据都那过来,需要的时候,才拿。
回到例子
看到这里,开始的例子应该大概可以有点清晰了,核心语句就是:
fornin[1,10]: base=(add(i,n)foriinbase)
在执行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上手动看看执行过程。
小结
概括
主要介绍了大概这样几点:
1.iterable,iterator与itertion的概念
2.迭代器协议
自定义可迭代对象与迭代器分离,保证数据复用
3.生成器:特殊的迭代器,内部实现了迭代器协议
其实这一块,那几个概念搞清楚,,这个很关键,搞懂了后面就水到渠成了。而且对之前的知识也有很多加深。
比如常见list就是iterator与iteable分离实现的,本身是可迭代对象,但不是迭代器,类似与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/