【Python】 迭代器&生成器

迭代器

  任何一个类,只要其实现了__iter__方法,就算是一个可迭代对象。可迭代对象的__iter__方法返回的对象是迭代器,迭代器类需要实现next方法。一般来说,实现了__iter__方法的类肯定还会顺便实现next方法,也就是说这个类既是一个可迭代对象也是个迭代器。

  一个迭代器ite可用ite.next()方法来返回其定义好的以某种算法找到的下一个元素,内建的iter(...)函数可把可迭代对象转化为迭代器。最常见的利用可迭代对象和迭代器的就是for语句了:

  for item in iterable这句话,首先调用了iterable这个对象的__iter__方法,返回一个迭代器(which在很多情况下就是iterable对象本身,就像上面说的,一个可迭代对象的类趋向于实现next让它成为自身的迭代器)。根据迭代器的next方法的算法一个接一个的提供值,把这个值赋给item,然后让item去进行循环体的操作。

  如果要自定义一个迭代器or可迭代对象的类,可以这样来:

class test(object):
    def __init__(self,num=10):
        self.num = num
        self.li = range(num)
        self.count = 0

    def __iter__(self):    #一般做法就是让__iter__直接返回自身就好了
        return self

    def next(self):
        try:
            if self.count == 0:
                return self.li[0]
            elif self.count <= self.num-1:
                return self.li[self.count]
            else:
                raise StopIteration    #调用迭代器的语句并不知道迭代什么时候才能完成,其往往是按照next给出的逻辑一直迭代下去直到出错。
                        #想让迭代器在某个边际下停止继续迭代的话,就需要在合适的条件时raise StopIteration来停止迭代。
                       #这个raise出来的并不是会输出到stderr的异常或错误,而是让调用迭代器的语句知道,可以不用再调用next方法来获取下一个值了
finally: self.count+=2 #这里还有一个比较tricky的,和迭代器本身没啥关系的点。。之前我在写count+=2的时候发现,无论写哪里都不太好。
                 #因为按照现有语句的逻辑,最好是在next方法返回值后count能再加一次2,这样下次进next万一超过了num-1的限制就可以不用再return。
                 # 但是一般而言,return之后的语句在本次调用函数的过程中时肯定不会再被执行了,除了try/finally结构。
                 # 就是说,finally中的语句,即便是在return执行了之后还是会被执行的。这点比较难想到!
if __name__ == '__main__': test = test() for item in test: print item

 

生成器

  任何带有yield语句的函数都是生成器。yield通常用在这样一个场景:在(多重)循环中,每遍历出一个单位,不立刻对其做处理,而是统一整合在一起再来处理。生成器是一种创建起来较为简单的迭代器。

  从流程上来说,相比于传统的res=[]然后循环地res.append这样的做法,生成器的特点是每次执行到yield语句就会暂停执行并将当前函数执行的上下文保存下来,然后先给出一个暂时的、局部的结果,不继续往下执行。比如yield出现在一个循环里的话不马上进行下一轮循环。那么下轮循环什么时候才会做?要等到外部调用这个生成器的next方法时执行。通过下面这个闭包的例子可以比较好地说明yield语句的这种性质:

def main():
    flist = []
    for i in range(3):
        def foo(x):
            print x + i
        flist.append(foo)

for func in main():
    func(2)

  这段代码的结果是什么?可能我们会下意识的以为是2,3,4。但是由于python函数的“后期绑定”特点(详情可看闭包那篇),foo中的i的具体的值并不是在函数定义的过程中确认,而是在foo被调用的时候才被最终确认。所以,第一次foo被调用的时候,此时的i已经是2了,所以最终结果是4,4,4。而如果是用生成器的机制,那么可以将这段代码改正:

def main():
    for i in range(3):
        def foo(x):
            print x+i
        yield foo

for func in main():
    func(2)

  由于main()是一个生成器,第一次执行到yield foo的时候,i确实还只是0,此时并不直接往下执行,而是暂停了运行并保存运行上下文。此时通过func(2)调用foo函数,i是0,所以可以得到正确结果。然后for func in main()进入下一轮循环,相当于调用了main()生成器的next方法,于是生成器激活了之前保存着的上下文继续运行,此时i得以加一,又开始一个类似的流程。最终,通过生成器搞出来的这个是可以生成2,3,4这样符合预期的结果的。

 

  调用具有生成器性质的函数后,返回的是一个可迭代对象(一般函数有return语句的话返回的就是一个实在的东西)。所以可以对他进行for item in 生成器 的操作。

  比如如下示例:

def flatten(nested_thing):
    for element in netsed_thing:
        if not isinstance(element,list):
            yield element
        else:
            for subelement in flatten(element):
                yield subelement

nested = [[1,2],[3,4],5,[[6,7],8]]
result = list(flatten(nested))
print result

#结果是[1,2,3,4,5,6,7,8]

  这个函数的功能是把很多重嵌套的列表展开铺平成一个列表。可以看到flatten函数是一个生成器,返回一个可迭代对象(所以可以在递归那里有for subelement in flatten(element))。从这个示例中我们还可以看出,在递归中,yield掉的东西是不分递归层级统一放置的,所以在第一层级yield到的5,和第二层级(一次递归)yield到的1234等,以及第三层级(二次递归)yield到的东西全部都放到了同一个可迭代对象里面了。

■  生成器的send方法

  自Python2.5版本之后,生成器拥有了send方法。

  上面用了很多的yield语句,但是似乎没有明确,yield语句本身返回的内容是什么。潜意识中,我们将yield语句等同于return语句,而执行到return语句的时候就会结束函数执行,因此无所谓return语句会返回什么内容。然而yield语句并不完全这样。send方法就是在某个合适的时候手工指定生成器中yield语句的返回值,并要求生成器继续执行(相当于调用next方法)。这样就可以使得生成器从原先只能通过next读取其内容的“只读”模式变成了next和send结合操作生成器的“读写”模式。另外,在下面的代码中我们可以看到,其实加上send之后,凸显出了生成器具有一定的协程的性质。

  下面是示例代码:

def main():
    strValue = None
    for i in range(10):
        if strValue:
            print strValue.format(i)
        strValue = yield i

if __name__ == '__main__':
    numGen = main()
    for num in numGen:
        print num
        if num % 2 == 0:
            numGen.send('next is odd number: {}')

 

  main()是一个比较简单的生成器。在__main__中,我们首先将main()这个生成器对象叫做numGen方便后面进行基于生成器的操作。main()被调用后首先yield到的i是0,由于此前的strValue并没有值,所以不输出,然后生成器执行暂停,返回上层。在上层中接下来我们通过for num in numGen开始进入循环,首先第一个num是0,所以打印出0后,还要进入if条件块。在这里就用到了生成器的send方法。调用send方法,第一步是将send参数中的字符串作为上次暂停时yield语句的返回值写到生成器中。其次就是要求生成器继续执行,相当于调用了next方法。结合这两点,因此send之后我们再次回到生成器中,strValue被赋予了一个字符串值。然后生成器继续执行,进入下一个循环。此时i变成了1,而strValue通过format方法的内容填充之后,打印出了next is odd number: 1。这样一个字符串。下面,又要yield,因此生成器再次暂停,回到上层。此时注意,回到上层的地方,是在numGen.send方法之后。如果send后面还有一些其他处理,那么要继续执行。这里没有,直接进入for循环的下一轮。进入循环下轮,其实又执行了一次生成器的next方法。我们再次进入了生成器。此时由于不是send方法要求的继续执行,所以strValue被重新赋值为None。生成器中循环进入下轮,i变成2,if因为条件不符合直接跳过,所以接下来又是yield……如此周而复始。最终获得到的stdout的输出是这样的:

0
next is odd number: 1
2
next is odd number: 3
4
next is odd number: 5
6
next is odd number: 7
8
next is odd number: 9

 

   如果读懂了上面一大堆文字描述,那么其实可以感觉得到,生成器的代码和外层调用生成器的“主函数”,两者形成了一个协程关系、我不执行的时候你继续执行,你不执行的时候我继续执行。另外可以注意到,在这段代码中,要求生成器调用next获取下一个值的入口可以有两个,一个是之前常用的for...in...循环,另一个就是send方法。一个比较有趣的现象是,如果将生成器中的for i in range(10)中10改成一个奇数,比如9,会导致报错StopIteration。我觉得这主要是因为迭代器的next方法会在所有可用元素都被使用但再次被调用的时候raise起StopIteration这个异常。而for...in...形式的调用的next对这个错误进行了处理。但是send并没有,因此会导致假如调用next的发起方刚好是send的时候这个错误被抛出。解决办法也很简单,就是在send外面包一层错误处理,当碰到StopIteration的时候直接break出for num in numGen这个循环。

■  关于递归中的yield

  自己编写带有yield的递归函数时,很容易走进这么一个坑。还是以上面的函数为例,我们很可能会写成:

def flatten(nested): 
    for element in nested:
        if not isinstance(element,list):
            yield element
        else:
            flatten(element)    # 注意这里

 

  这么写很明显,是期望在递归调用flatten函数的过程中,下层执行时的yield也能将结果收割出来到上层。但是生成器的结果是暂存在本层级函数调用栈中的(https://www.cnblogs.com/coder2012/p/4990834.html这篇是从Python源码角度分析生成器的文章,虽然看不懂,但是值得强行看一看【捂脸】),因此这个函数的最终结果就是只能返回一个去掉了所有非简单元素的列表。比如[5]。

  另一方面,上面正确的函数中,将下层生成器中的结果通过一个for/in语句,然后再度yield,相当于将下层生成器中的结果又收割到了本层的调用栈中。自然,最终就可以返回所有的结果了。

posted @ 2017-03-25 14:10  K.Takanashi  阅读(244)  评论(0编辑  收藏  举报