从迭代协议说起
在Python中,如果一个对象能够使用for … in …
语句进行迭代获取该对象中的子元素,那么该对象实际上是因为实现了Python中的迭代协议。
所谓“协议”,就是一套规范,一些约定,只要大家都遵守这些约定,那么一切就都会相安无事。
Iterable对象和Iterator对象
一个类的定义如果包含了__iter__()
方法,那么这个类创建出来的对象就叫做一个Iterable对象。(可迭代的对象)
一个类的定义如果包含了__iter__()
方法和__next__()
方法,那么这个类创建出来的对象就叫做一个Iterator对象。(迭代器对象)
一个类的定义如果包含了__iter__()
方法,那么这个类创建出来的对象将隐式成为Iterable类的一个实例。
一个类的定义如果包含了__iter__()
方法和__next__()
方法,那么这个类创建出来的对象将隐式成为Iterator对象的一个实例。(当然,由于包含__iter__()
方法,这个对象也是Iterable类的一个实例)
就是说,你可以通过isinstance(instanceVar, Iterable)
和 isinstance(instanceVar, Iterable)
来判断实例instance是不是一个Iterable对象或者Iterator对象。
可迭代的对象(Iterable对象)就好比是一个容器,是用来盛装子元素的。
迭代器对象(Iterator对象)是一个封装了一套从容器中取子元素算法的对象,即你可以将一个容器(某个可迭代对象)丢给这个迭代器,这个迭代器就会按照自己的一套规则来不断地取出这个容器中的元素。
__iter__()
函数和__next__()
函数的内容
根据迭代协议约定,Iterable对象的 __iter__()
函数应当无任何参数,并应当构造并返回一个Iterator的实例,仅此而已。并且一般来说,在构造Iterator对象时,要把Iterable对象中要让Iterator进行操作的子元素们的引用传递给Iterator,不然的话Iterator这套取子元素的算法该操作的对象是谁?
根据迭代协议约定,Iterator对象的 __next__()
函数应当无任何参数,并在每次__next__()
函数被调用的时候都从自己被构建时获得的要被迭代的元素们的引用中根据自身取元素的算法来取一个元素并将其返回。在某一次__next__()
函数调用中依据自身取元素算法发现已经没有元素可以继续返回了的时候,便应当抛出一个StopIteration异常。
for … in …
语法是如何践行迭代协议的
先介绍两个函数:iter()
和 next()
这两个函数分别要求传入一个Iterable对象和一个Iterator对象,实际上,这两个函数的作用就是帮我们调用指定Iterable对象的__iter__()
方法和指定Iterator对象的__next__()
方法。
即
iter(IterableObject)
等价于 IterableObject.__iter__()
next(IteratorObject)
等价于 IteratorObject.__next__()
由于Iterable对象的__iter__()
方法返回了一个构造好的Iterator对象,所以我们现在写出以下代码:
iteratorObject = iter(iterableObject)
这个Iterator对象iterableObject在被构造的时候已经获得了构造它的Iterable对象IterableObject中的子元素们的引用。以便于该Iterator对象根据自身算法进行
由于Iterator对象的__next__()
方法在每一次被调用时都会取出其在被构建时获得的要被迭代的元素们中的一个,直到没有元素可以取出后抛出一个StopIteration
异常,所以我们可以写出如下代码:
iteratorObject = iter(iterableObject)
while True:
try:
element = next(iteratorObject)
print(f"the next element is {element}")
except StopIteration:
break
实际上,我们已经根据迭代协议模拟出了一个for … in …
迭代的完整流程.其实,以上代码就等价于:
for element in iterableObject:
print(element)
没错,就是我们最常用的 for … in …
迭代循环,只不过Python在背后帮我们做了以上的工作,节省了我们的代码量。
让我们来捋一便 for … in …
语法帮我们在背后做了什么工作,首先在迭代开始前先调用iter()函数获得该Iterable对象的__iter__()
方法构造好的Iterator对象,然后开始不断地通过next()函数来调用这个Iterator对象的__next__()
方法以获得下一个迭代值,赋予给for 后边的变量,直到遇到__next__()
方法抛出的StopIteration
异常便停止for循环,此时迭代结束。
来个实际例子
我们定义一个Classroom类,我们希望这个类是一个容器,能够装进很多students。
我们应该可以通过一个addStudent(stu_name)
方法向该教室添加学生,之后我们希望我们可以通过for … in … 来依次遍历迭代这个教室里的所有学生。
即:
# 创建Iterable的一个实例
classroom = Classroom()
# 向这个容器中添加元素
classroom.addStudent("XiaoMing")
classroom.addStudent("XiaoHong")
classroom.addStudent("XiaoLiang")
# 根据这个容器返回的Iterator遍历这个容器里的元素
for stu in classroom:
print(stu)
让我们来实现这个Classroom的可迭代对象类
class Classroom(object):
def __init__(self):
# 用来存储一个Classroom的students
self.students = []
def addStudent(self, stu_name):
"""向这个教室添加学生"""
self.students.append(stu_name)
def __iter__(self):
"""根据迭代协议,返回一个迭代器,
注意这里要把教室内所有的学生的列表传给这个迭代器,
不然这个迭代器该迭代谁?"""
return StudentsIterator(self.students)
这个Classroom容器为需要一套迭代方法来迭代自身的元素,即还需要一个迭代器,让我们来实现这个学生迭代器的类。
class StudentsIterator(object):
def __init__(self, iterable_obj):
# 要迭代的对象容器
self.iterable_obj = iterable_obj
# 当前迭代到哪一个了
self.current_index = 0
def __iter__(self):
"""Iterator的__iter__()方法我们先空着"""
pass
def __next__(self):
"""根据迭代协议的要求,
__next__()函数应当在每一次被调用时
根据自身的定义的迭代规则从自身被创建时获得的元素集合中
拿出一个元素来作为方法返回值返回,
若没有元素可以返回了,则抛出StopIteration异常"""
if self.current_index < len(self.iterable_obj): # 如果迭代未完成
# 取下一个要迭代的元素值
next_element = self.iterable_obj[self.current_index]
# 顺序依次迭代
self.current_index += 1
# 将获取到的元素值返回给for中的变量
return next_element
else: # 如果已经没有元素可以迭代了,即迭代应当结束了
# 抛出StopIteration异常,for会捕获这个异常并结束迭代
raise StopIteration
一种合并了Iterable类和Iterator类的写法
网上有很多文章在讲Iterable对象和Iterator对象的时候都是这么写的:不是像我上边那样把Iterable对象类和Iterator对象类分开定义成两个类,而是直接合并成一个类来定义,Iterable对象的__iter__()
函数本应该返回一个Iterator对象,但是由于现在这个类本身就包含了__next__()
方法,即这个类本身除了是Iterable对象的类又是一个Iterator对象的类,所以__iter__()
方法直接返回自身的引用self
就相当于返回了一个Iterator对象的引用了。
class Classroom(object):
def __init__(self):
self.students = []
self.iterable_obj = iterable_obj
self.current_index = 0
def addStudent(self, stu_name):
self.students.append(stu_name)
def __iter__(self):
"""注意,这里本来应该返回一个Iterator的
但是这次这里直接返回了self,即返回了自身的引用"""
return self
def __next__(self):
if self.current_index < len(self.iterable_obj):
next_element = self.iterable_obj[self.current_index]
self.current_index += 1
return next_element
else:
raise StopIteration
emm…… 这种写法怎么说呢,的确是节省了代码量,但是个人感觉比较容易让不清楚迭代协议详细内容的小伙伴变得一头雾水,建议对迭代协议有了一个比较详细的了解了以后,再这么写来节省代码量。
为什么Iterator对象也规定需要包含一个
__iter__()
方法?
可以参考知乎上这个讨论
python的迭代器为什么一定要实现__iter__方法?
结语
for … in …
迭代语法的本质又是一个python的语法糖,python抛弃了类C语言中 for(int i=0; i<length; i++){}
这种遍历方法,在背后为我们做了大量的工作,简化了语法,节省了我们的时间和头发。
研究了一天终于写完了555…… 😂
如果有不严谨的地方欢迎批评指正