2016.3.10关于例子解释的补充更新html
源自个人博客python
老规矩,先上一个代码:git
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里面的生成器。github
要说生成器,必须首先说迭代器python3.x
讲到迭代器,就须要区别几个概念:iterable
,iterator
,itertion
, 看着都差很少,其实否则。下面区分一下。数组
itertion
: 就是迭代
,一个接一个(one after another),是一个通用的概念,好比一个循环遍历某个数组。app
iterable
: 这个是可迭代对象
,属于python的名词,范围也很广,可重复迭代,知足以下其中之一的都是iterable
:函数
能够for
循环: for i in iterable
学习
能够按index
索引的对象,也就是定义了__getitem__
方法,好比list,str
;code
定义了__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__' 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
这样,由这几个例子能够解释清楚这几个概念的区别。
说到这里,迭代器对象基本出来了。下面大体说一下,如何让自定义的类的对象成为迭代器对象,其实就是定义__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
从数据中分离出来,分别定义一个iterable
与iterator
以下:
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
的处理。
迭代器大概就记录到这里了,下面开始一个特殊的更加优雅的迭代器: 生成器
首先须要明确的就是生成器也是
iterator
迭代器,由于它遵循了迭代器协议.
yield
的函数生成器函数跟普通函数只有一点不同,就是把 return
换成yield
,其中yield
是一个语法糖,内部实现了迭代器协议,同时保持状态能够挂起。以下:
记住一点,yield
是数据的生产者,而诸如for
等是数据的消费者。
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这样说的:
惰性计算想像成水龙头,须要的时候打开,接完水了关掉,这时候数据流就暂停了,再须要的时候再打开水龙头,这时候数据还是接着输出,不须要从头开始循环
我的理解就是就是能够利用生成器来做为数据管道使用,当被检索的时候,每次拿出一个数据,而后向下面传递,传到最后,再拿第二个数据,在下面的例子中会详细说明。
其实本质跟迭代器差很少,不一次性把数据都那过来,须要的时候,才拿。
看到这里,开始的例子应该大概能够有点清晰了,
核心语句就是:
def gen(): for i in range(4): yield i 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]
.
新思路
这个能够以管道的思路来理解,首先gen()
函数是第一个生成器,下一个是第一次循环的base = (add(i, n) for i in base)
,最后一个生成器是第二次循环的base = (add(i, n) for i in base)
。
这样就至关于三个管道依次链接,可是水(数据)尚未流过,如今到了list(base)
,就至关于驱动器,打开了水的开关,这时候,按照管道的顺序,由第一个产生一个数据,yield 0
,而后第一个管道关闭。
以后传递给第二个管道就是第一次循环,此时执行了add(0, 10)
,而后水继续流,到第二次循环,再执行add(10, 10)
,此时到管道尾巴了,此时产生了第一个数据20,而后第一个管道再开放:yield 1
, 流程跟上面的同样,依次产生21,22,23; 直到没有数据。
把代码改一下容易理解:
def gen(): for i in range(4): yield i # 第一个管道 base = (add(i, 10) for i in base) # 第二个管道 base = (add(i, 10) for i in base) # 第三个管道 list(base) # 开关驱动器
具体执行过程能够在pythontutor上:
以前的解释被误导的缘由是,可能会误觉得是在第二个管道就把gen()
执行完毕了,其实不是这样的。
这种写法的好处显而易见:内存占用低。在数据量极大的时候,用list
就只能爆内存,而用生成器模式则彻底不用担忧
主要介绍了大概这样几点:
iterable
,iterator
与itertion
的概念
迭代器协议
自定义可迭代对象与迭代器分离,保证数据复用
生成器: 特殊的迭代器,内部实现了迭代器协议
其实这一块, 那几个概念搞清楚, ,这个很关键, 搞懂了后面就水到渠成了。并且对以前的知识也有不少加深。
好比常见list
就是iterator
与iteable
分离实现的,自己是可迭代对象,但不是迭代器, 相似与xrange
,可是又不一样。
愈来愈明白,看源码的重要性了。