Python学习笔记31:迭代技术
- 本系列文章的代码都存放在Github项目:python-learning-notes。
- 这一部分内容是《Fluent Python》目前为止最长的篇幅,我也花了大半天时间来阅读,内容的确庞杂,所以在提炼整理上可能会有所疏漏,请多包涵。
迭代技术无疑在Python中占有相当的地位。平时我们在写代码的时候,大多数时间也是话费在for
或者foreach
之类的循环语句上,而Python更进一步,在语言结构中直接整合了迭代技术,让我们可以更容易地在不同类型间使用类似的简单语法就可以进行迭代操作。
接下来我们通过Python迭代技术的核心概念:可迭代对象、迭代器和生成器来深入理解。
可迭代对象和迭代器
在Python学习笔记27:类序列对象中我们简单说明过可迭代对象和迭代器,现在我们从语言设计层面来理解这两个概念,先来看一下UML类图:
相当简单,Iterable
抽象类代表可迭代对象,Iterator
抽象类代表迭代器。
其中可迭代对象仅有一个抽象方法,返回一个迭代器。而迭代器有两个方法,抽象方法__next__
作为主要的迭代逻辑,将依次返回元素,完成迭代。而继承自可迭代对象的__iter__
方法被重写,用这样的方式实现:
def __iter__(self):
return self
这是因为我们在Python中会大量使用for/in
或者__init__
之类的情况来接收和使用可迭代对象,在进行迭代的时候后解释器会调用可迭代对象的__iter__
获取一个迭代器,进行具体迭代工作,而这个迭代器本身自然也可以作为一个可迭代对象来使用,所以才会是以上的这种类结构。
下面我们具体分析一下可迭代对象和迭代器。
可迭代对象
__iter__
上面我们说了,可迭代对象必须要实现__iter__
,而我们之前Python学习笔记27:类序列对象在实现过序列协议,只实现了__getitem__
和__len__
,但是依然可以迭代。
这是因为“Python偏爱序列”。
实际上是因为Python为了兼容旧的序列协议作出的妥协,对于没有实现__iter__
方法的序列,解释器会自动尝试通过__getitem__
并使用从零开始的下标来构建一个迭代器。
我们用下面这个例子说明:
import re
import reprlib
class Sentence():
RE_WORD = re.compile('\w+')
def __init__(self, text) -> None:
self.text = text
self.words = Sentence.RE_WORD.findall(text)
def __getitem__(self, index):
return self.words[index]
def __len__(self):
return len(self.words)
Sentence
是一个简单序列,接收字符串,并分割成单词。
测试一下:
from sentence import Sentence
s = Sentence("Today is a good day!")
for word in s:
print(word)
sIter = iter(s)
print(sIter)
for word in sIter:
print(word)
# Today
# is
# a
# good
# day
# <iterator object at 0x000002870BB7B4C0>
# Today
# is
# a
# good
# day
可以看到,虽然Sentence
没有实现__iter__
方法,但依然可以正常迭代,并且还可以用iter
函数获取到迭代器,获取到的迭代器同样可以正常用于迭代。
但需要注意的是,虽然Python通过这种方式兼容了序列,但是在事实上序列和可迭代对象是没有继承关系的,我们可以通过下面的代码进行验证:
from collections import abc
print(isinstance(s,abc.Iterable))
print(issubclass(Sentence,abc.Iterable))
# False
# False
迭代器
Python的迭代器相当简洁,只有两个方法,使用的时候也是同样简单:
sIter = iter(s)
already = []
while True:
print("{!s}^{!s}".format(already, sIter))
item = next(sIter)
already.append(item)
# []^<iterator object at 0x000001ABA4B02F70>
# ['Today']^<iterator object at 0x000001ABA4B02F70>
# ['Today', 'is']^<iterator object at 0x000001ABA4B02F70>
# ['Today', 'is', 'a']^<iterator object at 0x000001ABA4B02F70>
# ['Today', 'is', 'a', 'good']^<iterator object at 0x000001ABA4B02F70>
# ['Today', 'is', 'a', 'good', 'day']^<iterator object at 0x000001ABA4B02F70>
# Traceback (most recent call last):
# File "D:\workspace\python\python-learning-notes\note31\test.py", line 29, in <module>
# item = next(sIter)
# StopIteration
这里用
^
表示当前迭代指针的位置。
我们使用next
逐步从迭代器中获取元素,在获取完最后一个元素后,会抛出一个StopIteration
异常,如果我们使用的是for/in
语句,解释器会自动处理这个异常并退出迭代,如果是上面这样手动迭代,就需要处理这个异常:
sIter = iter(s)
already = []
while True:
print("{!s}^{!s}".format(already, sIter))
try:
item = next(sIter)
except StopIteration:
break
already.append(item)
# []^<iterator object at 0x000001D67CD6B4F0>
# ['Today']^<iterator object at 0x000001D67CD6B4F0>
# ['Today', 'is']^<iterator object at 0x000001D67CD6B4F0>
# ['Today', 'is', 'a']^<iterator object at 0x000001D67CD6B4F0>
# ['Today', 'is', 'a', 'good']^<iterator object at 0x000001D67CD6B4F0>
# ['Today', 'is', 'a', 'good', 'day']^<iterator object at 0x000001D67CD6B4F0>
还有一点我们需要注意,Python的迭代器仅实现了一个__next__
方法,很简洁,这是优点。但同时意味着功能单一,比如很多其他语言中的迭代器支持“重置”操作,我们可以随时重置迭代器然后从头重新迭代,但Python不行,当一个迭代器迭代完毕后就不能继续使用了,如果你还需要,只能再次使用iter
函数获取一个新的迭代器。
迭代器和可迭代对象其实是一种设计模式:迭代器模式。只不过Python通过深入内置这种设计模式让它和Python融为一体。
同样的,我们可以使用“粗苯”的方式来手动实现一个迭代器模式来说明这其中的机制。
迭代器模式
from ast import Index
import re
class SentenceV2():
RE_WORD = re.compile('\w+')
def __init__(self, text) -> None:
self.text = text
self.words = SentenceV2.RE_WORD.findall(text)
def __iter__(self):
return StenceIterator(self)
class StenceIterator():
def __init__(self, sentence) -> None:
self.sentence = sentence
self.index = 0
def __next__(self):
try:
result = self.sentence.words[self.index]
except IndexError:
raise StopIteration
self.index += 1
return result
def __iter__(self):
return self
我们实现了经典的迭代器模式,这里的StenceIterator
就是一个具体的迭代器。
我们看下测试结果:
from sentence_v2 import SentenceV2,StenceIterator
s = SentenceV2("Today is a good day!")
sIter = iter(s)
already = []
while True:
print("{!s}^{!s}".format(already, sIter))
try:
item = next(sIter)
except StopIteration:
break
already.append(item)
print(issubclass(SentenceV2, abc.Iterable))
print(isinstance(s, abc.Iterable))
print(issubclass(StenceIterator, abc.Iterator))
print(isinstance(sIter, abc.Iterator))
# []^<sentence_v2.StenceIterator object at 0x000002200527E550>
# ['Today']^<sentence_v2.StenceIterator object at 0x000002200527E550>
# ['Today', 'is']^<sentence_v2.StenceIterator object at 0x000002200527E550>
# ['Today', 'is', 'a']^<sentence_v2.StenceIterator object at 0x000002200527E550>
# ['Today', 'is', 'a', 'good']^<sentence_v2.StenceIterator object at 0x000002200527E550>
# ['Today', 'is', 'a', 'good', 'day']^<sentence_v2.StenceIterator object at 0x000002200527E550>
# True
# True
# True
# True
可以正常迭代,而且StenceV2
和SentenceIterator
的类对象和实例都通过了issubclass
和isinstance
函数的类型检查。
这事因为我们在Python学习笔记28:从协议到抽象基类中介绍过的subclasshook
。
简单地说就是通过subclasshook
告诉解释器实现了__iter__
方法的类都被视为Iterable
的子类,实现了__next__
和__iter__
的类都被视为Iterator
的子类。
对
subclasshook
如何定义感兴趣的可以阅读Python学习笔记28:从协议到抽象基类。
事实上上面的例子只是说明迭代器模式的机制,对于将Sentence
改造为纯粹的可迭代对象其实很简单:
import re
class SentenceV3():
RE_WORD = re.compile('\w+')
def __init__(self, text) -> None:
self.text