Python 的迭代器和生成器
PorSymonds
8年前
<p style="text-align:center"><img src="https://simg.open-open.com/show/6afda16c3fe2e7d44c8516e2543bacd8.jpg"></p> <p>事实上,当你在使用Python的第一天,你很有可能就已经和迭代器打交道了。这篇文章从浅入深的聊聊Python的迭代器和生成器。</p> <h3><strong>1. 从for语句开始</strong></h3> <p>有一定C语言基础的朋友在刚刚学习Python的时候,常常会询问Python有没有类似这样的写法:</p> <pre> <code class="language-python">for i = 0; i < 10 ; i += 1: print i</code></pre> <p>更有意思的是,当被告知『没有』的时候,一些人就打起了歪主意,写出了合法的代码:</p> <pre> <code class="language-python">i = 0 while i < 10: print i i += 1</code></pre> <p>这是用Python写C程序,非常的不推荐。实际上,for总是和in关键字一起使用</p> <pre> <code class="language-python">for i in range(10): print i</code></pre> <p>range不是Python的关键字,而是一个内建的函数(Python 2.X),它实际上返回了一个列表</p> <pre> <code class="language-python">>>> print range(10) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]</code></pre> <p>可是一些老司机还推荐这样写(仅限于Python 2.X)</p> <pre> <code class="language-python">for i in xrange(10): print i</code></pre> <p>想了解其中的奥义,继续阅读本文吧!</p> <p><strong>2.迭代器</strong></p> <p>有一些Python对象,我们可以从中按一定次序提取出其中的元素。这些对象称之为可迭代对象。比如,字符串、列表、元组都是可迭代对象。</p> <p>我们回忆一下从可迭代对象中提取元素的过程。这次,我们显式的使用列表的 <strong>下标</strong></p> <pre> <code class="language-python">my_str = 'abc' for i,_ in enumerate(my_str): print my_str[i]</code></pre> <p>同样的,以下是通过下标对可迭代对象的元素进行改写:</p> <pre> <code class="language-python">my_list = [1,2,3] for i,_ in enumerate(my_list): my_list[i] = my_list[i] + 1 print my_list</code></pre> <p>我们知道,在以上两个例子中,读取和写入元素,是通过可迭代对象的操作符[]实现的。而下标只是作为参数出现。我们不妨把这种模式,称作『可迭代对象是一等公民』。</p> <p>与之相对的,有没有可能将下标作为一等公民?换句话说,元素的提取只和下标打交道,而和可迭代对象无关。答案是有的。这样的一种 <strong>设计模式</strong> ,就是 <strong>迭代器模式</strong> 。那个升级版的下标,称之为 <strong>迭代器</strong> 。</p> <p>下面的代码,说明了如何显式的使用迭代器。利用Python内建函数iter,可以得到一个可迭代对象的 <strong>迭代器</strong> 。</p> <pre> <code class="language-python">my_list = [1,2,3] i = iter(my_list) while True: try: print next(i) except StopIteration: break</code></pre> <p>一旦迭代器建立起来,提取元素的过程就不再依赖于原始的可迭代对象,而是仅仅依赖于迭代器本身。Python内建的next函数作用于迭代器上,会执行三个操作:</p> <ol> <li>返回当前『位置』的元素,第一次next调用,当前位置是可迭代对象的起始位置</li> <li>将『位置』向后递增</li> <li>如果到达可迭代对象的末尾,即没有元素可以提取,则抛出StopIteration异常</li> </ol> <p>实际上,你并不需要这么麻烦的方法来使用迭代器,Python中的循环语句会自动进行迭代器的建立、next的调用和StopIteration的处理。换句话说,遍历一个可迭代对象的元素,这样写就对了:</p> <pre> <code class="language-python">my_list = [1,2,3] for v in my_list: print v</code></pre> <p>换句话说,Python的for...in语句,隐藏了大量的细节。</p> <p>所以,Python 2.X 的 range和xrange有何区别?答案是,range的返回值就是一个list,在你调用range的时候,Python会产生所有的元素。而xrange是一个特别设计的可迭代对象,它在建立的时候仅仅保存终止值。你可比较以下两种写法的实际运行结果:</p> <pre> <code class="language-python">for v in range(1000000000000): #possible Memory Error if v == 2: break for v in xrange(1000000000000): #fine if v == 2: break</code></pre> <p>在Python 3.X 中,不再有内建的xrange,其range等效于Python 2.X 的xrange</p> <p><strong>3. 自定义迭代器</strong></p> <p>那么Python的iter函数和next函数都做了什么呢?答案是,基本上什么都没做!它们的内部实现是(逻辑上)这样的:</p> <pre> <code class="language-python">def iter(obj): return obj.__iter__() #Python 2.X def next(obj): return obj.next() #Python 3.X def next(obj): return obj.__next__()</code></pre> <p>所以利用这一点,我们很容易写一个自己的可迭代对象和迭代器。下面就是一个例子,这个迭代器有随机的长度。测试它的方法就用for...in...</p> <pre> <code class="language-python">import random class demo_iterator(object): def __next__(self): return self.next() def next(self): v = random.randint(0,10) if v < 5: raise StopIteration() else: return v class demo_iterable(object): def __iter__(self): return demo_iterator() for v in demo_iterable(): print(v)</code></pre> <p>为了使得它在Python 2和Python 3都可用,我们同时实现了next和__next__。</p> <p>这个故事告诉我们,若想让你自己的对象支持for...in...,你可以实现它的迭代器接口。</p> <p>使用迭代器有何好处?用默认的列表不也很好吗?实际上,使用迭代器最大的优点是,能够及时处理『未知』的事件(例如,用户的输入,网络上的信号),并在迭代推进的过程中随时可以终止迭代。就拿range为例,我们如果想尽可能多的利用系统内存产生尽可能多的数据。那么使用迭代器的方法,可以在每一次迭代的时候都检查一下剩余可用的内存,从而决定要不要进行下一次迭代。而使用list的方法,过程中使用了多少内存,是很难预见的。</p> <p><strong>4 生成器</strong></p> <p>生成器是创建迭代器的一种简便的方法。生成器是一个特殊的函数。我们可以从静态和动态两个角度理解生成器函数。</p> <p>首先,从静态的角度,生成器函数在代码中表现为:</p> <ol> <li>含有yield语句(无论yield是否可能会被执行)</li> <li>无return或者仅有无值return(一旦函数里存在yield语句,有值return会视为语法错误)</li> </ol> <p>其次,从动态的角度,生成器函数在运行过程中:</p> <ol> <li>当生成器函数被调用的时候,生成器函数不执行内部的任何代码,直接立即返回一个迭代器。</li> <li>当所返回的迭代器第一次调用next的时候,生成器函数从头开始执行,如果遇到了执行yield x,next立即返回yield值x。</li> <li>当所返回的迭代器继续调用next的时候,生成器函数从上次yield语句的下一句开始执行,直到遇到下一次执行yield</li> <li>任何时候遇到函数结尾,或者return语句,抛出StopIteration异常</li> </ol> <p>特别的,生成器返回的迭代器,其__iter__返回其自身。</p> <p>举一个最简单的例子:</p> <pre> <code class="language-python">def g(): yield 1 yield 2 yield 3 for v in g(): print v</code></pre> <p>如何理解这个程序呢?</p> <p>把for循环展开,并在生成器函数中插入一些print语句。</p> <pre> <code class="language-python">def g(): print 'L1' yield 1 print 'L2' yield 2 print 'L3' yield 3 print 'L4' it = iter(g()) v = next(it);print v v = next(it);print v v = next(it);print v v = next(it);print v</code></pre> <p>g()已经返回了一个迭代器,iter这个迭代器将得到迭代器自身。所以it依然是生成器函数g返回的迭代器。</p> <pre> <code class="language-python">it = g() print id(it) print id(iter(it)) #same value as previous line</code></pre> <p>在执行g()的时候,并没有输出L1。而是第一次调用next的时候,L1输出,next返回1。以此类推,之后L4被打印出来,最后一个next抛出SropIteration异常。</p> <p>利用生成器,我们可以重写之前的随机序列,看起来简单多了。</p> <pre> <code class="language-python">import random def demo_generator(n): while True: v = random.randint(0,n) if v > 5: yield v else: break for i in demo_generator(10): print i</code></pre> <p><strong>5 应用、itertools、以及其他</strong></p> <p>迭代器由于其不定长的特性,特别适合表达数学中的『无穷序列』</p> <p>比如说,我们要寻找前10组直角三角形的边长。算法是暴利枚举每一边的长度。然而,我们并不知道边长的搜索边界,用列表做循环显然不合适。</p> <p>所以,我们首先产生一个正整数无穷序列:</p> <pre> <code class="language-python">def pint(): i = 1 while True: yield i i += 1</code></pre> <p>然后进行穷举。因为直角三角形的直角边总小于斜边,所以直角边的范围不用取无穷序列。另外,为了避免对称重复,最内层循环的直角边只取比另一个直角边小的值。</p> <pre> <code class="language-python">def tri(): for h in pint(): #hypotenuse for c1 in xrange(1,h): #cathetus1 for c2 in xrange(1,c1): #cathetus2 if c1 * c1 + c2 * c2 == h * h: yield (c1,c2,h)</code></pre> <p>tri也是个生成器,得到的迭代器也是个无穷序列。取前10个,用for循环就好:</p> <pre> <code class="language-python">for i,v in enumerate(tri()): if i == 10: break print v</code></pre> <p>实际上想取多少就取多少。这种无穷序列在解决数学问题的时候特别方便。</p> <p>Python内建库itertools有很多很方便的函数,大部分函数都支持无穷序列的运算。若自己写一些迭代器(无论用类的方法,还是用生成器),就可以很方便的利用这些itertools函数了。具体的计算功能可见相关文档。</p> <p>Python中除了可迭代对象,还有『容器』对象的概念。尽管很多内建对象即是容器又是可迭代对象,但这两个概念是相互独立的。容器对象无非是实现了__contains__成员,使得能够接受in操作符的运算。一个对象是不是容器,和它是不是可迭代,没有任何关系。</p> <pre> <code class="language-python">class cont(object): def __contains__(self,x): return True if 2 in cont(): print 'Here' for x in cont(): #TypeError: 'cont' object is not iterable print x</code></pre> <p> </p> <p>来自:https://zhuanlan.zhihu.com/p/24499698</p> <p> </p>