Python中的Iterable、Iterator、generator、yield

首先看一个例子:Python中list可迭代(Iterable),但是并不是迭代器(Iterator)。

from collections.abc import Iterable, Iterator
a = [1, 2, 3]
print(isinstance(a, Iterator))  # 结果为 False
print(isinstance(a, Iterable))  # 结果为 True

1.可迭代对象 Iterable

根据Python的迭代协议,只要类内部包含了iter这个魔法函数,那么这个类的对象就是可迭代对象。

from collections.abc import Iterable, Iterator
class A:
    def __iter__(self):
        pass
print(isinstance(A(), Iterable))    # 结果为 Frue
print(isinstance(A(), Iterator))    # 结果为 False

只要是可迭代对象,就可以使用for对其进行遍历。

for x in A():
    print(x)

但是类A中仅定义了iter函数,并没有具体实现,所以使用for对其进行遍历的时候出现了以下错误。

TypeError: iter() returned non-iterator of type 'NoneType'

很明显,iter魔法函数需要返回一个迭代器,所以我们对类A稍作修改。

class A:
    def __iter__(self):
        return iter([1, 2, 3, 4])

再使用for进行遍历的时候,以此打印出1、2、3、4。

2.迭代器 Iterator

一个类对象如果要成为迭代器,其类中必须同时包含iternext两个魔法函数。

from collections.abc import Iterable, Iterator
class B:
    def __init__(self):
        self.index = 0
        self.list = [1, 2, 3, 4, 5]
    def __iter__(self):
        return self
    def __next__(self):
        if self.index == len(self.list):
            raise StopIteration
        value = self.list[self.index]
        self.index += 1
        return value
print(isinstance(B(), Iterable))    # 结果为 True
print(isinstance(B(), Iterator))    # 结果为 True

但直接使用迭代器迭代的时候,如果完成了一轮遍历,就无法迭代了。
所以,通常在iter函数中返回一个迭代器。

from collections.abc import Iterable, Iterator
class B:
    def __iter__(self):
        return MyMyIterator()
        
class MyIterator(Iterator):
    def __init__(self):
        self.index = 0
        self.list = [1, 2, 3, 4, 5]

    def __next__(self):
        if self.index == len(self.list):
            raise StopIteration
        value = self.list[self.index]
        self.index += 1
        return value
        


if __name__ == '__main__':
    b = B()
    print(isinstance(b, Iterable))
    print(isinstance(b, Iterator))
    for x in b:
        print(x)
    # 因为__iter__()返回迭代器,所以可以反复遍历
    for x in b:
        print(x)

3.生成器 generator

3.1 使用生成器表达式创建生成器
print(type((x for x in range(1, 10))))  # 结果 <class 'generator'>
print(type((x for x in B())))  # 结果 <class 'generator'>
3.2 使用yeild创建生成器
def fib(index):
    n, a, b = 0, 0, 1
    while n < index:
        yield b
        a, b = b, a + b
        n += 1


if __name__ == '__main__':
    gen = fib(10)
    print(gen)
    print(type(gen))    # 结果 <class 'generator'>
    print(isinstance(gen, Iterable))    # True
    print(isinstance(gen, Iterator))    # True   

从最后的结果可以看到,generator也是Iterator,当然也是Interable(Iterator继承自Iterable)。

因此,可以将generator作为iter中的函数返回值,来构造一个Iterable。

将前面的Iterable类A进行改造如下:

class A:
    def __iter__(self):
        return (x for x in range(1, 5))

或者使用生成器函数

from collections.abc import Iterable, Iterator


def fib(index):
    n, a, b = 0, 0, 1
    while n < index:
        yield b
        a, b = b, a + b
        n += 1


class B:
    def __iter__(self):
        return fib(10)


b = B()
print(isinstance(b, Iterable))
print(isinstance(b, Iterator))
for x in b:
    print(x)
3.3 send、close、throw

生成器类中有个3个函数send、close、throw。

send: 生成器下一次迭代时,调用者向生成器中传递一个值。

close: 关闭生成器,如果继续迭代则会出现StopIteration异常。

trow: 主动向生成器抛出异常。

4.yield

上文提到了,包含yield的函数就是一个生成器,这实际上跟yield的特性有关。
yield可以让程序在一次迭代后暂停,下次迭代开始时程序还能在暂停的位置恢复并继续运行,而这刚好和生成器的特征吻合。

另外,上文提到了生成器的send函数可以使调用者向生成器传递值,而yield刚好能够接收到这个值。

def gen1():
    x = yield 1
    print('a', x)
    yield test(x)
    print('b', x)
    x = yield 3
    print('c', x)
    x = yield 4
    print('d', x)


if __name__ == '__main__':
    g1 = gen1()
    print(g1.send(None))
    print(g1.send(222))
    print(g1.send(333))
    print(g1.send(444))

上面的代码中,生成器迭代一次后,程序停在了yield 1位置;

然后第二次迭代时,通过send函数传入了值222,此时程序又从yield 1位置恢复,从方便理解的角度可以认为传入的值到了x = yield 1中的yield 1,也就是代码的等号右侧,根据代码逻辑,等号右侧的值赋值给了左侧的x

需要注意的时,生成器第一次迭代的时候,是无法传入值的,只能send(None)或者使用next()进行首次迭代,因为这个时候程序刚刚运行到yield这里,而不是从yield位置恢复。

5.yield form

yield from能够在暂停、恢复程序的同时,将Iterable对象转成generator。

def gen2():
    yield from [1, 2, 3, 4, 5]
    
if __name__ == '__main__':
    g2 = gen2()
    print(type(g2))
    for x in g2:
        print(x)