> 育儿
pythofo循环迭代(fo循环可以遍历可迭代对象)
导语:Python中的for循环、可迭代对象、迭代器和生成器
问题:
之前在学习list和dict相关的知识时,遇到了一个常见的问题:如何在遍历list或dict的时候正常删除?例如我们在遍历dict的时候删除,会报错:RuntimeError: dictionary changed size during iteration;而在遍历list的时候删除,会有部分元素删除不完全。由这个问题又引发了我对另一个问题的思考:我们通过for循环去遍历一个list或dict时,具体是如何for的呢?即for循环的本质是什么?在查阅了相关资料后,我认识到这是一个和迭代器相关的问题,所以借此机会来详细认识一下Python中的for循环、可迭代对象、迭代器和生成器1. 迭代
“迭代是重复反馈过程的活动,其目的通常是为了逼近所需目标或结果。”在Python中,可迭代对象、迭代器、for循环都是和“迭代”密切相关的知识点。
1.1 可迭代对象Iterable
在Python中,称可以迭代的对象为可迭代对象。要判断一个类是否可迭代,只需要判断这个类是否为Iterable类的实例即可:>>> from collections.abc import Iterable >>> isinstance([], Iterable) True >>> isinstance(123, Iterable) False 复制代码上述提供了一个判断对象是否为可迭代对象的方法,那么一个对象怎么才是可迭代对象呢——只需要该对象的类实现了__iter__()方法即可:>>> class A: pass >>> isinstance(A(), Iterable) False >>> class B: def __iter__(self): pass >>> isinstance(B(), Iterable) True 复制代码由此可见,只要一个类实现了__iter__()方法,那么这个类的实例对象就是可迭代对象。注意这里的__iter__()方法可以没有任何内容。1.2 迭代器Iterator
在Python中,通过Iterator类与迭代器相对应。相较于可迭代对象,迭代器只是多实现了一个__next__()方法:>>> from collections.abc import Iterator >>> class C: def __iter__(self): pass def __next__(self): pass >>> isinstance(C(), Iterator) True 复制代码显然,迭代器一定是可迭代对象(因为迭代器同时实现了__iter__()方法和__next__()方法),而可迭代对象不一定是迭代器。我们来看一下内建类型中的可迭代对象是否为迭代器:>>> isinstance(C(), Iterator) True >>> isinstance([], Iterable) True >>> isinstance([], Iterator) False >>> isinstance(&39;, Iterable) True >>> isinstance(&39;, Iterator) False >>> isinstance({}, Iterable) True >>> isinstance({}, Iterator) False 复制代码由此可见,str、list、dict对象都是可迭代对象,但它们都不是迭代器。至此,我们对可迭代对象和迭代器有了一个基本概念上的认识,也知道了有__iter__()和__next__()这两种方法。但是这两个魔法方法究竟是如何使用的呢?它们和for循环又有什么关系呢?1.3 for循环
1.3.1 iter()方法和next()方法
iter()方法和next()方法都是Python提供的内置方法。对对象使用iter()方法会调用对象的__iter__()方法,对对象使用next()方法会调用对象的__next__()方法。下面我们具体看一下它们之间的关系。1.3.2 iter()和__iter__()
__iter__()方法的作用就是返回一个迭代器,一般我们可以通过内置函数iter()来调用对象的__iter__()方法1.2中举的例子,只是简单的实现了__iter__()方法,但函数体直接被pass掉了,本质上是没有实现迭代功能的,现在我们来看一下__iter__()正常使用时的例子:>>> class A: def __iter__(self): print(&39;) return B() >>> class B: def __iter__(self): print(&39;) return self def __next__(self): pass >>> a = A() >>> a1 = iter(a) 执行A类的__iter__()方法 >>> b = B() >>> b1 = iter(b) 执行B类的__iter__()方法 复制代码可以看到,对于类A,我们为它的__iter__()方法设置了返回值为B(),而B()就是一个迭代器;而对于类B,我们在它的__iter__()方法中直接返回了它的实例self,因为它的实例本身就是可迭代对象。当然这里我们也可以返回其他的迭代器,但是如果__iter__()方法返回的是一个非迭代器,那么当我们调用iter()方法时就会报错:>>> class C: def __iter__(self): pass >>> iter(C()) Traceback (most recent call last): File &4>&39;NoneType<pyshell34;, line 1, in <module> iter(D()) TypeError: iter() returned non-iterator of type &39; 复制代码1.3.3 next()和__next__()
__next__()方法的作用是返回遍历过程中的下一个元素,如果没有下一个元素,则会抛出StopIteration异常,一般我们可以通过内置函数next()来调用对象的__next__()方法下面我们以list对象为例,来看一下next是如何遍历的:>>> l1 = [1, 2, 3] >>> next(l1) Traceback (most recent call last): File &1>&39;list&39;list&39;list_iterator<pyshell34;, line 1, in <module> next(l1_iter) StopIteration 复制代码思考:__next__()为什么要不停地去取出元素,并且在最后去抛出异常,而不是通过对象的长度相关信息来确定调用次数?个人认为是因为我们可以通过next()去手动调用对象的__next__()方法,而在next()中并没有判断对象的长度,所以需要在__next__()去处理1.3.4 自定义类实现__iter__()和__next__()
下面我们试着通过实现自定义一下list的迭代过程:
首先我们定义一个类A,它是一个可迭代对象,__iter__()方法会返回一个迭代器B(),并且还拥有一个成员变量m_Lst:>>> class A: def __init__(self, lst): self.m_Lst = lst def __iter__(self): return B(self.m_Lst) 复制代码对于迭代器的类B,我们实现它的__iter__()方法和__next__()方法,注意在__next__()方法中我们需要抛出StopIteration异常。此外,它拥有两个成员变量self.m_Lst和self.m_Index用于迭代遍历:>>> class B: def __init__(self, lst): self.m_Lst = lst self.m_Index= 0 def __iter__(self): return self def __next__(self): try: value = self.m_Lst[self.m_Index] self.m_Index += 1 return value except IndexError: raise StopIteration() 复制代码至此,我们已经完成了迭代器的准备工作,下面我们来实践一下迭代吧,为了更好地展示这个过程,我们可以加上一些打印:>>> class A: def __init__(self, lst): self.m_Lst = lst def __iter__(self): print(&39;) return B(self.m_Lst) >>> class B: def __init__(self, lst): self.m_Lst = lst self.m_Index= 0 def __iter__(self): print(&39;) return self def __next__(self): print(&39;) try: value = self.m_Lst[self.m_Index] self.m_Index += 1 return value except IndexError: print(&39;) raise StopIteration() >>> l = [1, 2, 3] >>> a = A(l) >>> a_iter = iter(a) call A().__iter__() >>> next(a_iter) call B().__next__() 1 >>> next(a_iter) call B().__next__() 2 >>> next(a_iter) call B().__next__() 3 >>> next(a_iter) call B().__next__() call B().__next__() except IndexError Traceback (most recent call last): File &5><pyshell34;, line 1, in <module> next(a_iter) File &5>&39;except StopIteration&39;first&39;second&39;third<pyshell34;, line 1, in <module> next(gen) StopIteration 复制代码这里我想给这个generator()函数加一个return,最后会在抛出异常时打印这个返回值(这里我对Python异常相关的知识了解比较少,不太清楚这个问题,以后再补充吧):>>> from collections.abc import Iterator >>> def generator(): print(&39;) yield 1 print(&39;) yield 2 print(&39;) yield 3 return &39; >>> gen = generator() >>> isinstance(gen, Iterator) True >>> next(gen) first 1 >>> next(gen) second 2 >>> next(gen) third 3 >>> next(gen) Traceback (most recent call last): File &7><pyshell34;, line 1, in <module> next(gen) StopIteration 复制代码至此,我们就有了生成器的两种创造方式:生成器函数(yield)返回一个生成器生成器解析式返回一个生成器3 解决问题
最后回到我们最初的问题:如何在遍历list或dict的时候正常删除?首先我们来探寻一下出错的原因,以list对象为例:>>> lst = [1, 2, 3] >>> for i in lst: print(i) lst.remove(i) 1 3 复制代码可以看到,我们在遍历打印列表元素的同时删除当前元素,实际的输出和我们需要的输出并不一样。以下是个人理解(想更准确地解答这个问题可能需要进一步结合源码):remove删除列表元素时,列表元素的索引会发生变化(这是因为Python底层列表是通过数组实现的,remove方法删除元素时需要挪动其他元素,具体分析我后续会补充相关源码学习笔记,这里先了解即可)类比我们自定义实现的迭代器,可以看到我们会在__next__()方法中对索引进行递增:>>> class A: def __init__(self, lst): self.m_Lst = lst def __iter__(self): print(&39;) return B(self.m_Lst) >>> class B: def __init__(self, lst): self.m_Lst = lst self.m_Index= 0 def __iter__(self): print(&39;) return self def __next__(self): print(&39;) try: value = self.m_Lst[self.m_Index] self.m_Index += 1 return value except IndexError: print(&39;) raise StopIteration() 复制代码那么我们可以猜测:列表对象对应的迭代器,应该也是会有一个索引成员变量,用于在__next__()方法中进行定位(这里没看过源码,只是个人猜想)当我们使用for循环遍历列表对象时,实际上是通过next()方法对其对应的迭代器进行操作,此时由于remove()方法的调用,导致列表元素的索引发生了改变(原来元素3的索引是2,删除元素2之后索引变为了1),所以在__next__()方法中,此时需要遍历的元素索引为1,而元素3顶替了这个位置,所以最后的输出为1,3。dict和list类似,不过在遍历时删除dict中的元素时会直接报错,具体原因大家也可以自行分析。本文内容由小故整理编辑!