本文首发于 at7h 的个人博客
这篇文章主要讨论下 Python 中的容器迭代协议,它的实现统一约定了容器对象的迭代方案,允许我们自定义类对迭代的支持。在 Python 中有很多的协议,比如迭代器协议,描述器协议等等,这些协议比较类似于其他面向对象语言中接口的概念。
本文主要介绍内容为可迭代对象、迭代器和生成器。
可迭代对象(iterable)
以通俗简单的方式来讲,可迭代对象就是能够逐一返回其成员项的对象,其可用于 for
循环以及许多其他需要一个序列的地方 zip
、map
等等。例如:
- 序列(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)
我的同名微信公众号: