第 8 章 Python可迭代对象

迭代器(iterator)有时又称游标(cursor)是程式设计的软件设计模式,可在容器物件(container,例如链表或阵列)上遍访的界面,设计人员无需关心容器物件的内存分配的实现细节。

8.1 可迭代对象

Python中经常使用for来对某个对象进行遍历,此时被遍历的这个对象就是可迭代对象,像常见的序列(字符串、列表、元组)、字典都是。如果给一个准确的定义的话,就是只要它定义了可以返回一个迭代器的__iter__方法,或者定义了可以支持下标索引的__getitem__方法,那么它就是一个可迭代对象。

8.2 迭代器

迭代器是通过next()来实现的,每调用一次他就会返回下一个元素,当没有下一个元素的时候返回一个StopIteration异常,所以实际上定义了这个方法的都算是迭代器。

8.3 生成器

生成器是构造迭代器的最简单有力的工具,与普通函数不同的只有在返回一个值的时候使用yield来替代return,然后yield会自动构建好next()iter()

8.4 三者之间关系

  1. 可迭代对象包含迭代器。
  2. 如果一个对象拥有__iter__方法,其是可迭代对象;如果一个对象拥有next方法,其是迭代器。
  3. 定义可迭代对象,必须实现__iter__方法;定义迭代器,必须实现__iter__next方法。

8.5 迭代器长度的计算

迭代器(包括生成器)是不能直接使用len()方法计算长度的,例如:

>>>l = (i for i in xrange(100) if i&1)

>>>len(l)

Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: object of type 'generator' has no len()

计算迭代器的长度,我们可以先将其转化为列表再计算,但如果迭代器规模较大,这将消耗大量内存,并不是很好的解决方案:

>>>len(list(l))

我们可以使用更为简洁的方式,即通过循环求和的方式得到迭代器的长度:

>>sum(1 for _ in l)
50

在此基础上,我们可以定义一个函数,专门用来求迭代器(生成器)的长度:

def leniter(iterator):
    """leniter(iterator): return the length of an iterator,consuming it."""
    if hasattr(iterator, "__len__"):
        return len(iterator)
    nelements = 0
    for _ in iterator:
        nelements += 1
    return nelements

8.6 yield

可以将yield简单类比为return,但是它除了返回一个值,还会记住这个返回的位置,下次迭代就从这个位置后(下一行)开始。yield与retrun语句最大的差别是,return语句之后的代码是不执行的,而yield语句之后的代码依然得到执行。

def yield_test(n):
    for i in range(n):
        yield call(i)
        print("i=",i)
        print("yield函数第 %d 次迭代结束"%(i))
    print('yield函数整体结束')

def call(i):
    return i*2
    
#使用for循环获取迭代器中的值
for i in yield_test(5):
    print('yield 函数的返回值是:',i)

上述代码的输出结果为:

yield 函数的返回值是: 0
i= 0
yield函数第 0 次迭代结束
yield 函数的返回值是: 2
i= 1
yield函数第 1 次迭代结束
yield 函数的返回值是: 4
i= 2
yield函数第 2 次迭代结束
yield 函数的返回值是: 6
i= 3
yield函数第 3 次迭代结束
yield 函数的返回值是: 8
i= 4
yield函数第 4 次迭代结束
yield函数整体结束

一个带有 yield 的函数就是一个 generator,它和普通函数不同,生成一个 generator 看起来像函数调用,但不会执行任何函数代码,直到对其调用 next()(在 for 循环中会自动调用 next())才开始执行。虽然执行流程仍按函数的流程执行,但每执行到一个 yield 语句就会中断,并返回一个迭代值,下次执行时从 yield 的下一个语句继续执行。看起来就好像一个函数在正常执行的过程中被 yield 中断了数次,每次中断都会通过 yield 返回当前的迭代值。

yield 的好处是显而易见的,把一个函数改写为一个 generator 就获得了迭代能力,比起用类的实例保存状态来计算下一个 next() 的值,不仅代码简洁,而且执行流程异常清晰。