雪花飘落

for循环和迭代器

先来看几个例子:

# 例1
ls = [1, 2, 3]
for i in ls:
    ls = [4, 5, 6]
    print(i)

# 例2
ls = [1, 2, 3]
for i in ls:
    ls.append(4)
    print(i)

# 例3
ls = [1, 2, 3]
for i in ls:
    ls.remove(i)
print(ls)

在例一中依然会输出1、2、3,例二中会输出1、2、3然后无限循环输出4,在例三中,会输出[2]。

 

迭代器

要搞清楚这个问题,我们首先得先来看看Python中的迭代器,所谓迭代器,是遍历访问容器对象(container)的一种手段,所有实现了迭代器的容器都有一个特征:当访问当前节点时,可以同时得到下一个节点的位置信息,这样,就无需关心容器中每个节点的具体分布规律和细节,只需要不断访问节点,依次往后遍历即可。迭代器有两个特点:

(1)迭代器不需要知道容器的长度,因为每次访问的位置信息都是从上一次访问的节点中得到的,所以具体什么时候到头只有真正访问到头了才知道。

(2)迭代操作不可逆,它只能访问“下一个”节点,没办法访问“上一个”节点。

iterable

可迭代对象,只要实现了__iter__方法,它就是一个可迭代对象,Python六种基本类型中,除了数字,都是可迭代对象。当对可迭代对象调用内建函数iter()的时候,会生成一个对应类型的迭代器(iterator)。也可以直接通过调用类对象方法的写法来创建这个迭代器。


#<class 'list_iterator'>

print(type(iter([1, 2, 3])))
print(type([1,2,3].__iter__()))

iterator

通过iter()函数可以生成一个对应类型的迭代器,而在迭代器当中,实现了一个叫做__next__()的方法,在程序执行的过程中每一次调用__next__()都会往后访问一个,并且不可逆。当然,也可以通过内建函数next()来调用它。

'''
1
2
'''
it = iter([1,2,3,4,5])
print(next(it))
print(it.__next__())

当访问到结构末尾时,若没有下一个节点了,调用next()会返回一个异常:StopIteration。

for循环

for循环遍历序列的本质,就是先生成序列的迭代器,然后通过迭代器去遍历序列。所以for i in [1,2,3,4,5]等价于:

it = [1, 2, 3, 4, 5].__iter__()
while True:
    try:
        i = it.__next__()
        print(i)
    except StopIteration:
        break

对例1的解释:

理解了上面for循环的本质之后,接下来的内容其实就比较简单了,比如在上面的例1中:

ls = [1, 2, 3]
for i in ls:
    ls = [4, 5, 6]
    print(i)

当执行for i in ls这一句之后,就已经创建了[1,2,3]这个列表的迭代器,用于遍历这个列表,注意这里我说[1,2,3]而不是ls,因为变量的本质是地址的别名,只是对数据存储地址的一个标签,对变量进行赋值其实就是对地址空间建立别名映射的过程。迭代器访问的是存在于这个地址的列表,而不是这个标签。所以当在循环内执行ls = [4,5,6]之后,其实ls这个标签已经被贴到列表[4,5,6]所在的地址上了,它并不会对原来的迭代器造成影响。所以输出还是1、2、3。

对例2的解释:

ls = [1, 2, 3]
for i in ls:
    ls.append(4)
    print(i)

在这个程序中,我们依然是生成了列表[1,2,3]的迭代器,不过在循环中,我们一直在往列表中添加元素,因为列表是可变类型,所以对列表元素的增加都是在往原来地址上的列表增加节点,那么迭代器每往后访问一个节点,这个列表就会新增一个节点,它永远不会遇到StopIteration,也就永远不会结束。

对例3的解释:

ls = [1, 2, 3]
for i in ls:
    ls.remove(i)
print(ls)

这个东西还是比较经典的坑了,一边遍历一边删除,其结果并不会全部删除,而是会间隔一个。因为列表是一个线性表,它的地址空间是连续的,如果删除了前面的元素,那后面的元素就会移动位置,来补齐这个空缺。可以模拟一下这个过程:

'''
ls = [1,2,3]
1、2、3分别存储在节点地址A、B、C上
A B C
1 2 3
迭代器现在访问到节点A,然后执行删除,删除之后,2、3会往前移动补齐空缺的位置:
A B C
2 3 
下一次循环,迭代器应该访问A的下一个节点,也就是节点B了,那现在找到节点B里面存储的数字3然后删除
A B C
2
当B节点访问完之后,节点C所在的地址已经不属于这个序列了,那本次迭代就结束了,所以程序例还会剩下数字2
'''

如果说我们从头开始删除,因为删除元素之后,会影响其它元素的地址,那如果每次删除末尾会如何呢?

ls = [1,2,3,4,5]
for i in ls:
    ls.pop()
print(ls)

结果是[1,2],和预想的也不一样,前面我们说过,迭代器只能从前往后,并且这个顺序是不可逆的,所以即使我们是从后往前删除,但迭代器依然是从前往后遍历的,所以:迭代器访问1的时候,删除了5,访问2的时候删除了4,访问3的时候删除了3,然后这个序列就遍历完成了,所以1和2被剩下了。

在遍历的同时删除

经过这两个例子,我们发现了其实一边遍历一边删除会出问题主要是迭代器的工作原理导致的,所以如果我们真的想一边遍历一边删除,那就要避免使用原序列的迭代器:

#通过遍历列表的浅拷贝来删除
ls = [1, 2, 2, 3, 3, 4, 5]
for i in ls[:]:
    ls.remove(i)
print(ls)

或者通过索引的方式,同时注意删除元素导致索引的改变:

#从后往前删除,避免索引改变造成的问题
ls = [1,2,3,4,5]
for i in range(len(ls)):
    ls.pop()
print(ls)

而像字典、集合这种哈希表,是不支持在迭代器遍历的同时进行增加或者删除操作的,会直接抛出错误:RuntimeError: dictionary\set changed size during iteration

 

posted @ 2022-11-04 14:07  haruyuki  阅读(863)  评论(0编辑  收藏  举报