彻底理解Python的可迭代对象、迭代器、生成器

首先,本文会先讲清楚可迭代对象(iterable)和迭代器(iterator)的关系问题,然后讲一下生成器(generator)的问题。

一、可迭代对象和迭代器

在Python中,for循环的执行、还有其他的一些函数比如sum()的执行,都是依赖于可迭代对象的。比如下面代码:

for i in someobject:
    pass

someobject就必须是一个可迭代对象,否则会报错。这个for循环背后的运行机制是,Python首先会调用someobject的__iter__()方法,得到该方法返回的一个迭代器对象,然后不停的调用该迭代器对象的next()方法,直到遇到抛出StopIteration的错误,迭代结束。

为了实现迭代的动作(不管是for循环还是sum等函数),Pyhton规定可迭代对象,一定要实现__iter__()方法,而且这个__iter__()方法必须要返回一个迭代器对象。

那么迭代器对象又是什么呢?迭代器对象最根本的职责就是实现next()方法,在该方法返回下一个要迭代的元素。

又因为Python中很多现有库中的函数,都是直接返回一个迭代器,比如itertools里面的izip等方法。现在试想一下,如果想在for循环中使用这些迭代器中的值,应该怎么办?肯定是需要定义一个可迭代对象,然后在这个可迭代对象的__iter__()方法中返回izip等方法所返回的那个迭代器。这样实在太麻烦了,为了方便在for循环中直接使用这些函数的返回的迭代器,就需要让迭代器(iterator)本身也是一个可迭代对象(iterable),也就是让迭代器对象也实现__iter__()方法。可想而知,迭代器对象的__iter__()方法中,返回的迭代器基本上是他自身(即return self)。因为就是为了要让Python代码在诸如for循环等操作中可以直接使用迭代器本身,为了方便,Python才规定迭代器要实现__iter__()方法,__iter__()方法当然要返回自身迭代器对象了。

综上,迭代器对象就是即实现了next()方法,又实现了__iter__()方法的对象,而且__iter__()方法中返回的肯定是他自身对象。可迭代对象就是实现了__iter__()方法的对象,可迭代对象的__iter__()方法返回的是某个迭代器(未必是它自身)。

行文至此,可以看到,for循环以及sum()函数等操作,都要求操作一个可迭代对象,但归根结底是要把操作落在某一个迭代器身上。那么有人就会有疑问了:既然终究是要操作迭代器,为什么不取消可迭代对象的概念,直接让所有的迭代器在__iter__()方法中返回它自身不就行了?这样for循环以及sum()函数等操作最终想操作哪个迭代器,就直接使用该迭代器对象不就可以了吗?

要想搞清楚这个问题,需要了解同一个迭代器对象只能迭代一次的特点,以下面代码为例:

class IterTest:
    def __init__(self):
        self.i = 0

def __iter__(self):
    return self


def next(self):
    if self.i < 10:
        i = self.i
        self.i = self.i + 1
        return i
    else:
        raise StopIteration()

x = IterTest()
print '======== iterate begin ========'
for i in x:
    print i

print '======== iterate again ========'
for i in x:
    print i
print '======== iterate end ========'

上面代码中,IterTest就是一个迭代器类,迭代返回0到9一共10个数。代码中x就是一个使用IterTest生成的迭代器对象,代码分别两次使用for循环对x对象进行了迭代。根据迭代器的特点,可以推测,第二次for循环是打印不出任何内容的。可以看一下该代码的执行结果:

result1

可以看到,第二个for循环确实没有迭代出任何数字。

但是在实际的编程工作中,很多时候都需要对一个可迭代对象进行多次迭代循环,还有可能是多个线程并行的循环同一个可迭代对象,如果这个可迭代对象是上面代码中这个迭代器本身,那么就实现不了多次迭代了,除非再新生成一个迭代器对象(比如再写一个y = IterTest())。为了解决这个问题,就需要把可迭代对象与迭代器对象分开定义,以下面代码为例:

class IterTest:
    def __init__(self):
        pass
    def __iter__(self):
        return IteratorNew()

class IteratorNew:
    def __init__(self):
        self.i = 0
    def __iter__(self):
        return self
    def next(self):
        if self.i < 10:
            i = self.i
            self.i = self.i + 1
            return i
        else:
            raise StopIteration()

x = IterTest()
print '======== iterate begin ========'
for i in x:
    print i

print '======== iterate again ========'
for i in x:
    print i
print '======== iterate end ========'

上面代码的可迭代对象是IterTest类的实例x,在for循环中,调用x对象的__iter__时,会创建一个IteratorNew类的实例并返回。这样一来,不论对x对象做几次for循环,都能迭代出数字来,因为每次for循环过程中,x对象返回的都是一个新的IteratorNew类的迭代器对象,这样就达到了同一个可迭代对象能够在代码中多处进行迭代的目的(关键就在于只要一调用__iter__()方法,就会返回一个新的迭代器对象)。其实Python中的list就是一个典型的可迭代对象,但是它不是迭代器本身,所以你可以在代码中随时用for循环来迭代同一个list。上述代码的执行结果如下:

result2

在这里可以补充一个知识点,Python中有一个可以返回一个迭代器的内置函数iter()。参数可以传一个支持迭代的集合对象,比如传入一个list。使用iter()函数得到的迭代器对象只能迭代一次,因为它的__iter__()函数肯定返回的是它自身,这意味着每次调用__iter__()时返回的都是同一个迭代器对象。示例代码如下:

lista = [1,2,3,4,5]

itera = iter(lista)

for i in itera:
    print i

print '===== again ====='

for i in itera:
    print i
print '===== end ====='

运行结果如下:

result3

综上,关于可迭代对象和迭代器对象的内容,总结一下:

  1. 可迭代对象是实现了__iter__()方法的对象。
  2. 迭代器对象是即实现了next()方法,又实现了__iter__()方法的对象。
  3. 迭代器对象本身就是一个可迭代对象,这个可迭代对象返回的迭代器是他自身。
  4. for循环以及sum()函数等操作,要求操作一个可迭代对象,但归根结底是要把操作落在某一个迭代器身上。这个迭代器与可迭代对象有可能是同一个对象,也有可能是不同的对象。
  5. 之所以把可迭代对象和迭代器对象区分开,是因为编程的时候有对同一个可迭代对象多次进行迭代的需求

需要注意一个现象:当迭代器类A里面没有实现__iter__()方法时(next()当然已经实现了),只要有另一个可迭代对象的__iter__()方法返回了该迭代器类A的实例,代码就能正常工作。这个迭代器类A应该不算是一个规范的迭代器,Python自己封装的代码中用到的迭代器类一定都是实现了__iter__()方法的。Python并没有在语法规则上对迭代器有什么特殊的处理,迭代器类跟其他类一样,内部def了一些方法,只不过如果真的用到了某个方法但是发现找不到该方法时,程序肯定会报错。比如下面代码,类IteratorNew中没有实现__iter__()方法,但可以正常工作:

class IterTest:
    def __init__(self):
        pass
    def __iter__(self):
        return IteratorNew()

class IteratorNew:
    def __init__(self):
        self.i = 0
    def next(self):
        if self.i < 10:
            i = self.i
            self.i = self.i + 1
            return i
        else:
            raise StopIteration()

x = IterTest()
print '======== iterate begin ========'
for i in x:
    print i

print '======== iterate again ========'
for i in x:
    print i
print '======== iterate end ========'

二、生成器

下面来谈谈生成器的问题。之所以先谈迭代器,再谈生成器,是因为生成器本身就是一个迭代器。它是Python语言对迭代器的进一步抽象。可以这样理解,生成器是实现一个迭代器的语法糖。我们都知道,所谓语法糖,就是那种清晰明了的编程语言语法,可以使你把心中想对计算机说的的千言万语,轻易地用编程语言表达出来。当你想定义一个迭代器时,需要写一个类,这个类要实现__iter__()方法,还要实现next()方法。Pytnon提供了生成器这种对象类型,可以让你方便的获得一个迭代器,这个迭代器就是生成器。不需要再像上述那么麻烦去定义一个类的方式来获得迭代器。

Python中,有两种方式来定义一个生成器,分别是生成器函数和生成器表达式,下面分别介绍。

2.1 生成器函数

生成器函数跟其他函数一样,都是函数。不同的是,这个函数内部有yield语句,没有写return语句。实际上生成器函数默认返回的是一个生成器对象,就是说是有return的,默认return了一个生成器对象。以如下代码为例:

def testGenerate():
    n = 0
    while n < 10:
        yield n
        n = n + 1

x = testGenerate()
print '===== the type of testGenerate ====='
print type(testGenerate)
print'===== the type of testGenerate() ====='
print type(x)

上述代码的执行结果为:

result4

可以看到,testGenerate的类型仍然是函数,而testGenerate ()函数调用后返回的结果的类型是generator类型,即生成器类型。testGenerate函数就叫做生成器函数。这里需要强调一下,testGenerate是一个函数,它是生成器函数,这个函数的返回的对象才是生成器。不要混淆了生成器函数和生成器。

将上述代码稍加丰富,变成以下代码,把生成器使用起来:

def testGenerate():
    n = 0
    while n < 10:
        yield n
        n = n + 1

x = testGenerate()
print '===== the type of testGenerate ====='
print type(testGenerate)
print'===== the type of testGenerate() ====='
print type(x)

print '===== show numbers ====='
for num in x:
    print num

print '===== show again ====='
for num in x:
    print num
print '===== end ====='

上述代码的执行结果如下:

result5

下面详细的说明生成器的工作过程:

  1. 首先,Python解释器一看testGenerate函数中有yield语句,就知道这是一个生成器函数。
  2. Python解释器会进行一系列的工作,把这个生成器函数转换为一个迭代器,包括__iter__()方法的实现,next()方法的实现,都是Python解释器自己完成的
  3. Python解释器在实现next()方法的时候,要参照testGenerate函数中的yield语句,每一次yield,都应该在next()方法中对应有返回一个该值的处理,也就是说testGenerate函数中yield的值就是生成的迭代器中每一次迭代得到的值。
  4. 最后Python解释器根据testGenerate函数生成的这个迭代器,就是生成器对象,作为testGenerate函数的返回值,返回给testGenerate函数的调用者。

通过上面流程的梳理,可以看到,生成器函数中的yield语句,并不是该生成器函数的返回值,而是根据该生成器函数生成的迭代器的next()方法的返回值;生成器函数的返回值是一个生成器类型的迭代器对象!

2.2?生成器表达式

生成器表达式与列表生成式类似,把列表生成式中的中括号换成圆括号即可。代码举例如下:

g = (x for x in range(10))   # 生成器表达式
print type(g)

for i in g:
    print i

代码执行结果:

result6

可以想到,当迭代元素的生成规则比较简单时,用生成器表达式比较方便;如果迭代元素的生成规则比较复杂,难以用一句代码来实现,那就定义一个生成器函数,在生成器函数中通过yield语句来实现迭代元素的生成。

2.3?生成器的好处

生成器的好处有如下几个:

  1. 节省内存,用到某一个值的时候,才生成这个值。不需要把所有的值事先存放到列表里,那样数据很多的时候很浪费内存。
  2. 代码简洁,在需要一个迭代器的时候,直接使用生成器来实现,避免了繁琐的迭代器类的定义。

Leave a comment

Your email address will not be published.