Python 可迭代对象, 迭代器与生成器

这篇文章主要讨论下 Python 中的容器迭代协议,它的实现统一约定了容器对象的迭代方案,允许我们自定义类对迭代的支持。在 Python 中有很多的协议,比如迭代器协议描述器协议等等,这些协议比较类似于其他面向对象语言中接口的概念。

本文主要介绍内容为可迭代对象、迭代器和生成器。

可迭代对象(iterable)

以通俗简单的方式来讲,可迭代对象就是能够逐一返回其成员项的对象,其可用于 for 循环以及许多其他需要一个序列的地方 zipmap 等等。例如:

  • 序列(sequence)类型: 如 list, str, tuple, bytes
  • 非序列类型类型如:dict, 文件对象等

严格来说,可迭代对象是任何实现了 __iter__() 方法或者实现了序列(Sequence)语义中的 __getitem__() 方法的任意自定义类对象

那么下面我们分别来讨论下这两种实现方案。

1. 实现 __iter__

这种方式是你必须为你的类实现 __iter__()方法,该方法返回的必须是一个迭代器对象,下面会讲到。

2. 实现 __getitem__

序列(Sequence)的迭代通过实现 __getitem__() 方法来使用整数索引进行高效的元素访问。同时,你必须为其定义一个返回序列长度的 __len__() 方法。例如上面例子中的 list, str, tuple, bytes。序列数据结构的存储是一段连续的内存空间,其直接使用整数索引进行寻址,查找元素非常高效,但是插入删除元素时效率低下。

有的童鞋会说,dict 也实现了 __getitem____len__ 协议,为毛 dict 不属于序列?原因是它并不是通过整数索引来顺序迭代,而是通过任意的不可变键(immutable key) 来进行逐个查找的。所以 dict 的迭代还是因为其实现了 __iter__

工作方式

当把一个可迭代对象 x 作为参数传给内置函数 iter() 时,它会返回该对象的迭代器以供迭代。但通常我们并不需要这么搞,当我们使用 for 对可迭代对象进行遍历时,for 语句会为你自动处理那些操作,即自动调用 iter(x) 来获取迭代器,若对象没有实现__iter__()方法,而实现了方式二的 __getitem____len__ 协议,其会自动创建一个迭代器,并尝试从索引 0 开始获取元素,若尝试失败,会引发一个 TypeError

类型断言

判断一个对象是否是一个可迭代对象,可以使用 collections.abc.Iterable

In [1]: from collections.abc import Iterable

In [2]: isinstance([1, 2, 3], Iterable)
Out[2]: True

In [3]: isinstance(1, Iterable)
Out[3]: False

In [4]: isinstance('1', Iterable)
Out[4]: True

In [5]: isinstance({}, Iterable)
Out[5]: True

迭代器(iterator)

上面多次提到了迭代器,那么什么是迭代器?即实现了迭代器协议的对象就是一个迭代器,迭代器协议由 __iter__()__next__() 共同组成。以 Golang 接口的思维理解就是任何实现了这俩方法的对象就是迭代器。

是的,又有 __iter__(),所以迭代器必定也是可迭代对象。在迭代器中:

  • __iter__() 必须返回迭代器对象本身。
  • __next__() 应从容器中返回下一项,如果已经没有数据项可返回时,则需引发 StopIteration 异常,继续调用其 __next__() 应再次引发 StopIteration 异常。

所以一个迭代器应有的样子应该是这样的:

class Iterator:

    def __iter__(self):
        return self

    def __next__(self):
        pass

工作方式

迭代器用来表示一连串数据流的对象,当 for 语句自动返回可迭代对象的迭代器时,for 将重复调用其 __next__() 方法将逐个返回流中的项,直到迭代器引发 StopIteration 异常后终止循环。此时该迭代器数据项已耗尽,不能再使用。我们可以通过将迭代器传给 next() 函数来模拟:

In [11]: iterator = iter([1, 2, 3]) # 手动创建一个迭代器

In [12]: for i in iterator:
   ...:     print(i)
   ...:
1
2
3

In [13]: next(iterator) # 该迭代器已耗尽
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-13-4ce711c44abc> in <module>
----> 1 next(iterator)

StopIteration:

In [14]: iterator = iter([1, 2, 3])

In [15]: next(iterator)
Out[15]: 1

In [16]: next(iterator)
Out[16]: 2

In [17]: next(iterator)
Out[17]: 3

In [18]: next(iterator)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-18-4ce711c44abc> in <module>
----> 1 next(iterator)

StopIteration:

类型

判断一个对象是否是一个迭代器,可以使用 collections.abc.Iterator

In [31]: from collections.abc import Iterator

In [32]: iterator = iter([1, 2, 3])

In [33]: isinstance(iterator, Iterator)
Out[33]: True

In [34]: isinstance([],Iterator)
Out[34]: False

练习

通过上面的介绍,你可能或多或少的了解了什么是可迭代对象 iterable,什么是迭代器 iterator。一句话简单的总结就是,当我们对可迭代对象进行迭代时,就从可迭代对象获取迭代器进行迭代。

下面我们来写一个非常简单的一段文本处理的示例,从下面一段 HTML 文本中提取出所有 a 标签的 href

TEXT = ('<div><a href="https://blog.python.org" title="More News">More</a><ul class="menu">'
        '<li><a href="http://feedproxy.google.com/~r/PythonSoftwareFoundationNew/~3/T3r7qZxo-xg'
        '/python-software-foundation-fellow.html">Python Software Foundation Fellow Members for'
        'Q3 2019</a></li><li> <a href="http://feedproxy.google.com/~r/PythonSoftwareFoundationNews'
        '/~3/lE0u-5MIUQc/why-sponsor-pycon-2020.html">Why Sponsor PyCon 2020?</a></li><li><a href='
        '"http://feedproxy.google.com/~r/PythonSoftwareFoundationNews/~3/jAMRqiPhWSs/seeking-developers'
        '-for-paid-contract.html">Developers for Paid Contract Improving pip</a></li></ul></div>')
class LinkFinder:

    PATTERN = "(?<=href=\").+?(?=\")|(?<=href=\').+?(?=\')"

    def __init__(self, text):
        self.links = re.findall(self.PATTERN, text)

    def __iter__(self):
        return LinkIiterator(self.links)

class LinkIiterator:

    def __init__(self, links):
        self.links = links
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        try:
            link = self.links[self.index]
        except IndexError:
            raise StopIteration
        self.index += 1
        return link

稍稍验证一下:

In [39]: for link in LinkFinder(TEXT):
    ...:     print(link)
    ...:
https://blog.python.org
http://feedproxy.google.com/~r/PythonSoftwareFoundationNew/~3/T3r7qZxo-xg/python-software-foundation-fellow.html
http://feedproxy.google.com/~r/PythonSoftwareFoundationNews/~3/lE0u-5MIUQc/why-sponsor-pycon-2020.html
http://feedproxy.google.com/~r/PythonSoftwareFoundationNews/~3/jAMRqiPhWSs/seeking-developers-for-paid-contract.html

生成器(generator)

看完上面的例子,有同学可能会想,能不能省掉 LinkIiterator 呢?有没有一种机制能够让 __iter__() 返回的对象自动提供 __iter__()__next__() 方法呢?

有,那就是生成器(generator)。

一个包含 yield 的函数就是一个生成器函数(通常称其为生成器),当其被调用的时,会返回一个迭代器,称为生成器迭代器(generator iterator)。yield 表达式会产生一系列值来供给 for 循环使用或是通过 next() 函数逐一获取。每个 yield 会临时暂停处理,记住当前位置执行状态(包括局部变量,指令指针,内部求值栈和任何异常处理的状态等),当该生成器迭代器恢复时,它会从离开位置继续执行,这与每次调用都从新开始的普通函数差别很大。因本文主要讲迭代协议,所以关于生成器的实现部分暂不具体讨论。

生成器机制提供了一种实现迭代器协议的便捷方式。 即如果我们把容器对象 __iter__() 方法实现为一个生成器,那么它将返回一个生成器迭代器对象(generator iterator),该对象提供 __iter__()__next__() 方法以供迭代。

我们将上面的例子改成生成器版:

class LinkFinder:

    PATTERN = "(?<=href=\").+?(?=\")|(?<=href=\').+?(?=\')"

    def __init__(self, text):
        self.links = re.findall(self.PATTERN, text)

    def __iter__(self):
        for link in self.links:
            yield link

验证下:

In [40]: for link in LinkFinder(TEXT):
    ...:     print(link)
    ...:
https://blog.python.org
http://feedproxy.google.com/~r/PythonSoftwareFoundationNew/~3/T3r7qZxo-xg/python-software-foundation-fellow.html
http://feedproxy.google.com/~r/PythonSoftwareFoundationNews/~3/lE0u-5MIUQc/why-sponsor-pycon-2020.html
http://feedproxy.google.com/~r/PythonSoftwareFoundationNews/~3/jAMRqiPhWSs/seeking-developers-for-paid-contract.html

优化

本小节其实属于多余部分,关于迭代协议的讨论基本已经结束,但通过上面关于生成器的介绍我们发现,生成器迭代器与普通迭代器的一个显著区别就是,其往往是随用随生成,而不是一次性生成完毕,这在迭代大量数据时将非常有用,不用一次性计算或载入全部的数据到内存。所以上面的例子还有待优化。因为 links 列表是一次性计算好的。可以使用 re.finditer 方法,对 links 进行惰性求值:

class LinkFinder:

    PATTERN = "(?<=href=\").+?(?=\")|(?<=href=\').+?(?=\')"

    def __init__(self, text):
        self.links = re.finditer(self.PATTERN, text)

    def __iter__(self):
        for link in self.links:
            yield link.group()

还可以使用一种更加简洁的方式,即使用生成器表达式

def __iter__(self):
    return (link.group() for link in self.links)