问题:之前在学习list和dict相关的知识时,遇到了一个常见的问题:如何在遍历list或dict的时候正常删除?例如我们在遍历dict的时候删除,会报错:RuntimeError:而在遍历list的时候删除,会有部分元素删除不完全。由这个问题又引发了我对另一个问题的思考:我们通过for循环去遍历一个list或dict时,具体是如何for的呢?即for循环的本质是什么?在查阅了相关资料后,我认识到这是一个和迭代器相关的问题,所以借此机会来详细认识一下Python中的for循环、可迭代对象、迭代器和生成器1。迭代 迭代是重复反馈过程的活动,其目的通常是为了逼近所需目标或结果。在Python中,可迭代对象、迭代器、for循环都是和迭代密切相关的知识点。1。1可迭代对象Iterable在Python中,称可以迭代的对象为可迭代对象。要判断一个类是否可迭代,只需要判断这个类是否为Iterable类的实例即可:fromcollections。abcimportIterableisinstance(〔〕,Iterable)Trueisinstance(123,Iterable)False复制代码上述提供了一个判断对象是否为可迭代对象的方法,那么一个对象怎么才是可迭代对象呢只需要该对象的类实现了iter()方法即可:classA:passisinstance(A(),Iterable)FalseclassB:defiter(self):passisinstance(B(),Iterable)True复制代码由此可见,只要一个类实现了iter()方法,那么这个类的实例对象就是可迭代对象。注意这里的iter()方法可以没有任何内容。1。2迭代器Iterator在Python中,通过Iterator类与迭代器相对应。相较于可迭代对象,迭代器只是多实现了一个next()方法:fromcollections。abcimportIteratorclassC:defiter(self):passdefnext(self):passisinstance(C(),Iterator)True复制代码显然,迭代器一定是可迭代对象(因为迭代器同时实现了iter()方法和next()方法),而可迭代对象不一定是迭代器。我们来看一下内建类型中的可迭代对象是否为迭代器:isinstance(C(),Iterator)Trueisinstance(〔〕,Iterable)Trueisinstance(〔〕,Iterator)Falseisinstance(123,Iterable)Trueisinstance(123,Iterator)Falseisinstance({},Iterable)Trueisinstance({},Iterator)False复制代码由此可见,str、list、dict对象都是可迭代对象,但它们都不是迭代器。至此,我们对可迭代对象和迭代器有了一个基本概念上的认识,也知道了有iter()和next()这两种方法。但是这两个魔法方法究竟是如何使用的呢?它们和for循环又有什么关系呢?1。3for循环1。3。1iter()方法和next()方法iter()方法和next()方法都是Python提供的内置方法。对对象使用iter()方法会调用对象的iter()方法,对对象使用next()方法会调用对象的next()方法。下面我们具体看一下它们之间的关系。1。3。2iter()和iter()iter()方法的作用就是返回一个迭代器,一般我们可以通过内置函数iter()来调用对象的iter()方法1。2中举的例子,只是简单的实现了iter()方法,但函数体直接被pass掉了,本质上是没有实现迭代功能的,现在我们来看一下iter()正常使用时的例子:classA:defiter(self):print(执行A类的iter()方法)returnB()classB:defiter(self):print(执行B类的iter()方法)returnselfdefnext(self):passaA()a1iter(a)执行A类的iter()方法bB()b1iter(b)执行B类的iter()方法复制代码可以看到,对于类A,我们为它的iter()方法设置了返回值为B(),而B()就是一个迭代器;而对于类B,我们在它的iter()方法中直接返回了它的实例self,因为它的实例本身就是可迭代对象。当然这里我们也可以返回其他的迭代器,但是如果iter()方法返回的是一个非迭代器,那么当我们调用iter()方法时就会报错:classC:defiter(self):passiter(C())Traceback(mostrecentcalllast):Filepyshell4,line1,initer(C())TypeError:iter()returnednoniteratoroftypeNoneTypeclassD:defiter(self):return〔〕iter(D())Traceback(mostrecentcalllast):Filepyshell8,line1,initer(D())TypeError:iter()returnednoniteratoroftypelist复制代码liul1。3。3next()和next()next()方法的作用是返回遍历过程中的下一个元素,如果没有下一个元素,则会抛出StopIteration异常,一般我们可以通过内置函数next()来调用对象的next()方法下面我们以list对象为例,来看一下next是如何遍历的:l1〔1,2,3〕next(l1)Traceback(mostrecentcalllast):Filepyshell1,line1,innext(l1)TypeError:listobjectisnotaniterator复制代码可以看到,当我们直接对列表对象l1使用next()方法时,会报错listobjectisnotaniterator,显然list对象并不是迭代器,也就是说它没有实现next()方法,那么我们怎么才能去对一个列表对象使用next()呢根据我们前面介绍的iter()方法,我们知道它会返回一个迭代器,而迭代器是实现了next()方法的,所以我们可以先对list对象使用iter(),获取到它对应的迭代器,然后对这个迭代器使用next()就可以了:l1〔1,2,3〕l1iteriter(l1)type(l1iter)next(l1iter)1next(l1iter)2next(l1iter)3next(l1iter)Traceback(mostrecentcalllast):Filepyshell6,line1,innext(l1iter)StopIteration复制代码li思考:next()为什么要不停地去取出元素,并且在最后去抛出异常,而不是通过对象的长度相关信息来确定调用次数?个人认为是因为我们可以通过next()去手动调用对象的next()方法,而在next()中并没有判断对象的长度,所以需要在next()去处理ul1。3。4自定义类实现iter()和next() 下面我们试着通过实现自定义一下list的迭代过程:首先我们定义一个类A,它是一个可迭代对象,iter()方法会返回一个迭代器B(),并且还拥有一个成员变量mLst:classA:definit(self,lst):self。mLstlstdefiter(self):returnB(self。mLst)复制代码对于迭代器的类B,我们实现它的iter()方法和next()方法,注意在next()方法中我们需要抛出StopIteration异常。此外,它拥有两个成员变量self。mLst和self。mIndex用于迭代遍历:classB:definit(self,lst):self。mLstlstself。mIndex0defiter(self):returnselfdefnext(self):try:valueself。mLst〔self。mIndex〕self。mIndex1returnvalueexceptIndexError:raiseStopIteration()复制代码至此,我们已经完成了迭代器的准备工作,下面我们来实践一下迭代吧,为了更好地展示这个过程,我们可以加上一些打印:classA:definit(self,lst):self。mLstlstdefiter(self):print(callA()。iter())returnB(self。mLst)classB:definit(self,lst):self。mLstlstself。mIndex0defiter(self):print(callB()。iter())returnselfdefnext(self):print(callB()。next())try:valueself。mLst〔self。mIndex〕self。mIndex1returnvalueexceptIndexError:print(callB()。next()exceptIndexError)raiseStopIteration()l〔1,2,3〕aA(l)aiteriter(a)callA()。iter()next(aiter)callB()。next()1next(aiter)callB()。next()2next(aiter)callB()。next()3next(aiter)callB()。next()callB()。next()exceptIndexErrorTraceback(mostrecentcalllast):Filepyshell5,line11,innextvalueself。mLst〔self。mIndex〕IndexError:listindexoutofrangeDuringhandlingoftheaboveexception,anotherexceptionoccurred:Traceback(mostrecentcalllast):Filepyshell12,line1,innext(aiter)Filepyshell5,line16,innextraiseStopIteration()StopIteration复制代码可以看到,我们借助iter()和next()方法能够很好地将整个遍历的过程展示出来。至此,我们对可迭代对象、迭代器以及iter()和next()都有了一定的认识,那么,for循环和它们有什么关系呢?1。3。5探究for循环for循环是我们使用频率最高的操作之一,我们一般会用它来遍历一个容器(列表、字典等),这些容器都有一个共同的特点都是可迭代对象。那么对于我们自定义的类A,它的实例对象a应该也可以通过for循环来遍历:foriina:print(i)callA()。iter()callB()。next()1callB()。next()2callB()。next()3callB()。next()callB()。next()exceptIndexErrorforiina:passcallA()。iter()callB()。next()callB()。next()callB()。next()callB()。next()callB()。next()exceptIndexError复制代码通过打印,我们可以清楚的看到:对一个可迭代对象使用for循环进行遍历时,for循环会调用该对象的iter()方法来获取到迭代器,然后循环调用该迭代器的next()方法,依次获取下一个元素,并且最后会捕获StopIteration异常(这里可以尝试在类B的next()方法最后只捕获IndexError而不抛出StopIteration,则for循环此时会无限循环)既然我们提到了for循环会自动去捕获StopIteration异常,当没有捕获到StopIteration异常时会无限循环,那么我们是否可以用while循环来模拟一下这个过程呢?whileTrue:try:inext(aiter)print(i)exceptStopIteration:print(exceptStopIteration)breakcallB()。next()1callB()。next()2callB()。next()3callB()。next()callB()。next()exceptIndexErrorexceptStopIteration复制代码到这里,大家应该对for对可迭代对象遍历的过程有了一定的了解,想要更深入了解的话可以结合源码进一步学习(本次学习分享主要是结合实际代码对一些概念进行讲解,并未涉及到相应源码)。2生成器 迭代器和生成器总是会被同时提起,那么它们之间有什么关联呢生成器是一种特殊的迭代器。2。1获取生成器当一个函数体内使用yield关键字时,我们就称这个函数为生成器函数;当我们调用这个生成器函数时,Python会自动在返回的对象中添加iter()方法和next()方法,它返回的对象就是一个生成器。代码示例:fromcollections。abcimportIteratordefgenerator():print(first)yield1print(second)yield2print(third)yield3gengenerator()isinstance(gen,Iterator)True复制代码2。2next(生成器)既然生成器是一种特殊的迭代器,那么我们对它使用一下next()方法:next(gen)first1next(gen)second2next(gen)third3next(gen)Traceback(mostrecentcalllast):Filepyshell19,line1,innext(gen)StopIteration复制代码这里我想给这个generator()函数加一个return,最后会在抛出异常时打印这个返回值(这里我对Python异常相关的知识了解比较少,不太清楚这个问题,以后再补充吧):fromcollections。abcimportIteratordefgenerator():print(first)yield1print(second)yield2print(third)yield3returnendgengenerator()isinstance(gen,Iterator)Truenext(gen)first1next(gen)second2next(gen)third3next(gen)Traceback(mostrecentcalllast):Filepyshell7,line1,innext(gen)StopIteration:end复制代码可以看到,当我们对生成器使用next()方法时,生成器会执行到下一个yield为止,并且返回yield后面的值;当我们再次调用next(生成器)时,会继续向下执行,直到下一个yield语句;执行到最后再没有yield语句时,就会抛出StopIteration异常2。3生成器和迭代器通过上面的过程,我们知道了生成器本质上就是一种迭代器,但是除了yield的特殊外,生成器还有什么特殊点呢惰性计算。这里的惰性计算是指:当我们调用next(生成器)时,每次调用只会产生一个值,这样的好处就是,当遍历的元素量很大时,我们不需要将所有的元素一次获取,而是每次只取其中的一个元素,可以节省大量内存。(个人理解:这里注意和上面的迭代器的next()区别开,对于迭代器,虽然每次next()时,也只会返回一个值,但是本质上我们已经把所有的值存储在内存中了(比如类A和类B的self。mLst),但是对于生成器,内存中并不会将所有的值先存储起来,而是每次调用next()就获取一个值)下面我们来看一个实际的例子:输出10000000以内的所有偶数(注意,如果实际业务环境下需要存储,那就根据实际情况来,这里只是针对两者的区别进行讨论)首先我们通过迭代器来实现:(这里直接使用列表)defiterator():lst〔〕index0whileindex10000000:ifindex20:print(index)lst。append(index)index1returnlstresultiterator()复制代码然后通过生成器来实现:defgenerator():index0whileindex10000000:ifindex20:yieldindexindex1gengenerator()next(gen)0next(gen)2next(gen)4next(gen)6next(gen)8复制代码由于采取了惰性运算,生成器也有它的不足:对于列表对象、字典对象等可迭代对象,我们可以通过len()方法直接获取其长度,但是对于生成器对象,我们只知道当前元素,自然就不能获取到它的长度信息了。下面我们总结一下生成器和迭代器的相同点和不同点:生成器是一种特殊的迭代器;迭代器会通过return来返回值,而生成器则是通过yield来返回值,对生成器使用next()方法,会在每一个yield语句处停下;迭代器会存储所有的元素,但是生成器采用的是惰性计算,只知道当前元素。2。4生成器解析式列表解析式是我们常用的一种解析式:(类似的还有字典解析式、集合解析式)lst〔iforiinrange(10)ifi21〕lst〔1,3,5,7,9〕复制代码而生成器解析式和列表解析式类似,我们只需要将〔〕更换为()即可:(把元组解析式给抢了,hh)gen(iforiinrange(10)ifi21)genat0x00000193E2945A80next(gen)1next(gen)3next(gen)5next(gen)7next(gen)9next(gen)Traceback(mostrecentcalllast):Filepyshell11,line1,innext(gen)StopIteration复制代码li至此,我们就有了生成器的两种创造方式:生成器函数(yield)返回一个生成器生成器解析式返回一个生成器ul3解决问题最后回到我们最初的问题:如何在遍历list或dict的时候正常删除?首先我们来探寻一下出错的原因,以list对象为例:lst〔1,2,3〕foriinlst:print(i)lst。remove(i)13复制代码可以看到,我们在遍历打印列表元素的同时删除当前元素,实际的输出和我们需要的输出并不一样。以下是个人理解(想更准确地解答这个问题可能需要进一步结合源码):remove删除列表元素时,列表元素的索引会发生变化(这是因为Python底层列表是通过数组实现的,remove方法删除元素时需要挪动其他元素,具体分析我后续会补充相关源码学习笔记,这里先了解即可)类比我们自定义实现的迭代器,可以看到我们会在next()方法中对索引进行递增:classA:definit(self,lst):self。mLstlstdefiter(self):print(callA()。iter())returnB(self。mLst)classB:definit(self,lst):self。mLstlstself。mIndex0defiter(self):print(callB()。iter())returnselfdefnext(self):print(callB()。next())try:valueself。mLst〔self。mIndex〕self。mIndex1returnvalueexceptIndexError:print(callB()。next()exceptIndexError)raiseStopIteration()复制代码那么我们可以猜测:列表对象对应的迭代器,应该也是会有一个索引成员变量,用于在next()方法中进行定位(这里没看过源码,只是个人猜想)当我们使用for循环遍历列表对象时,实际上是通过next()方法对其对应的迭代器进行操作,此时由于remove()方法的调用,导致列表元素的索引发生了改变(原来元素3的索引是2,删除元素2之后索引变为了1),所以在next()方法中,此时需要遍历的元素索引为1,而元素3顶替了这个位置,所以最后的输出为1,3。dict和list类似,不过在遍历时删除dict中的元素时会直接报错,具体原因大家也可以自行分析。