迭代是数据处理的基石:程序将计算应用于数据序列。如果数据太大,在内存中放不下,则需要采用“惰性”的方式获取数据,即只按需获取和处理数据项的一部分,而不是一次性加载整个数据集。这就是迭代器的作用。
在Python中,所有容器类型都是可迭代对象。Python使用可迭代对象提供的迭代器支持以下操作:
- for循环
- 列表,字典,集合推导式
- 拆包赋值
- 构造容器实例
单词序列
首先我们通过下面代码开启可迭代对象之旅。
import re
import reprlib
RE_WORD = re.compile(r'\w+')
class Sentence:
def __init__(self, text: str):
self.text = text
self.words = RE_WORD.findall(text)
def __len__(self):
return len(self.words)
def __getitem__(self, index):
return self.words[index]
def __repr__(self):
# reprlib.repr这个实用函数用于生成大型模型结构的简略字符串表现形式
return f"Sentence({reprlib.repr(self.text)})"
我们测试一下上面的代码:
s = Sentence(text="静夜思:唐【李白】 床前明月光,疑是地上霜。举头望明月,低头思故乡。")
print(s) # 输出:Sentence('静夜思:唐【李白】 床前...。举头望明月,低头思故乡。')
for i in s:
print(i)
print(s[0]) # 静夜思
print(s[-1]) # 低头思故乡
print(list(s)) # 输出:['静夜思', '唐', '李白', '床前明月光', '疑是地上霜', '举头望明月', '低头思故乡']
上面代码中传入一个字符串,创建一个Sentence实例。reprlib.repr函数默认情况下生成的字符串最多有30个字符,所以多余的字符串使用...替代。Sentence实例可以迭代,因为可以迭代,使用Sentence对象可用于构建列表和其他可迭代类型。
Python开发者都知道,序列可以迭代,为什么呢?
序列可以迭代的原因:iter函数
需要迭代对象x时,Python会自动调用iter(x)。
内置函数iter执行以下操作:
- 检查对象是否实现了__iter__方法,如果实现了就调用它,获取一个迭代器。
- 如果没有实现__iter__方法,但是实现了__getitem__方法,那么iter()创建一个迭代器,尝试索引(从0开始)获取项。
- 然后尝试失败,则Python抛出TypeError异常,通常会提示“'C' object is not iterable”,其中C是目标对象所属的类。
所有Python序列可迭代的原因是,按照定义,序列都实现了__getitem__方法(不懂的话可以去看看前面的《深入剖析序列的特殊方法》一文)。其实,标准的序列也都实现了__iter__方法。因此不仅实现了特殊方法__iter__的对象被视为可迭代对象,实现了__getitem__方法的对象也被视为可迭代对象。
from collections import abc
class Demo:
def __getitem__(self, item):
pass
class Demo1:
def __iter__(self):
pass
print(iter(Demo())) # <iterator object at 0x000001E61560C610>
print(isinstance(Demo1(),abc.Iterable)) #True
值得一提的是,在Python3.10开始,检查对象x是否迭代,最准确的方法是使用iter(x)函数,如果不可迭代,则处理TypeError异常。这比isinstence(x,abc.Iterable)更准确,因为iter(x)函数还会考虑过时的__getitem__方法,而抽象基类Iterable则不考虑。
内置的iter()更常被Python自身使用,我们自己很少用到。iter()函数还有另一种用法,不过鲜为人知。
使用iter处理可调用对象
调用iter()时,传入两个参数可以为函数或任何可迭代对象创建迭代器。对于这种用法,第一个参数必须是一个可迭代对象,重复调用产生值;第二个参数是哨符,即一种标记值,如果可调用对象返回哨符,则迭代器抛出StopIteration,而不产出哨符。
例如下面示例:使用iter函数掷一个6面骰子,直到点数为1.
import random
def d6():
return random.randint(1, 6)
d6_iter = iter(d6, 1)
print(d6_iter)
for i in d6_iter:
print(i)
输出:
<callable_iterator object at 0x0000016A13F82C80>
4
3
5
6
2
5
3
2
4
上面的iter函数返回的是一个callable_iterator。这个示例中的for循环可能会运行很长时间,但是绝对不会产生1,因为1是哨符。
可迭代对象和迭代器
由上面内容可知,可迭代对象:使用内置函数iter可以获取迭代器的对象。如果对象实现了能返回迭代器的__iter__方法,那么对象就是可迭代的。序列都可以迭代。实现了__getitem__方法,而且接受从0开始的索引,这种对象也可以迭代。
可迭代对象和迭代器之间的关系是:Python从可迭代对象中获取迭代器。
Python中字符串是可以迭代的。表面上看不出来,但是背后有一个迭代器。
s = 'ABC'
for char in s:
print(char)
假如没有for语句,我们不得不使用while循环模拟:
s='ABC'
s_iter = iter(s)
while True:
try:
print(next(s_iter))
except StopIteration:
del s_iter # 释放s_iter的引用,即废弃迭代器对象
break
StopIteration表明迭代器已耗尽。内置函数iter()在内部自行处理for循环和其他迭代上下文(列表推导式,可迭代对象拆包等)的StopIteration异常。
Python标准的迭代器接口有以下两个方法:
- __next__:返回序列的下一项,如果没有项了,抛出StopIteration异常
- __iter__:返回self,以便在预期可迭代对象的地方使用迭代器。例如for循环中
回到上面的Sentence类:
s = Sentence(text="静夜思:唐【李白】 床前明月光,疑是地上霜。举头望明月,低头思故乡。")
it = iter(s)
print(it) # <iterator object at 0x0000026DBBCD3460>
print(next(it)) # 静夜思
print(next(it)) # 唐
print(list(it)) # ['李白', '床前明月光', '疑是地上霜', '举头望明月', '低头思故乡']
可以清楚的看出iter()是如何构建迭代器的,以及next()是如何使用迭代器的,以及无法重置已经耗尽的迭代器。
为Sentence类实现__iter__方法
下面实现经典迭代器设计模式。虽然不符合Python的习惯做法,后续会重构。但是通过下面代码,可以明确可迭代容器和迭代器之间的关系。
import re
import reprlib
RE_WORD = re.compile(r'\w+')
class Sentence:
def __init__(self, text: str):
self.text = text
self.words = RE_WORD.findall(text)
def __len__(self):
return len(self.words)
def __repr__(self):
# reprlib.repr这个实用函数用于生成大型模型结构的简略字符串表现形式
return f"Sentence({reprlib.repr(self.text)})"
def __iter__(self):
return SentenceIterator(self.words)
class SentenceIterator:
def __init__(self, words):
self.words = words
self.index = 0
def __next__(self):
try:
word = self.words[self.index]
except IndexError:
raise StopIteration()
self.index += 1
return word
def __iter__(self):
return self
与前一版相比,这里只多了一个__iter__方法,没有了__getitem__方法,为的是明确表明这个类之所以可以迭代,是因为实现了__iter__方法。根据可迭代协议,__iter__方法实例化并返回一个迭代器(SentenceIterator)。
注意:对于上面的SentenceIterator迭代器示例来说,没必要在类中实现__iter__方法,不过这样做是对的,因为迭代器应该实现__iter__和__next__这两个方法,而且这样做能让迭代器通过issubclass(SentenceIterator,abc.Iterator)测试。
不要把可迭代对象变成迭代器
构建可迭代对象和迭代器时经常会出现错误,原因是混淆了二者。要知道,可迭代对象有个__iter__方法,每次都实例化一个新的迭代器;而迭代器要实现__next__方法,返回单个元素,此外还要实现__iter__方法,返回本身。
因此迭代器是可迭代对象,但是可迭代对象不是迭代器。