老齐教室

深入理解for循环

在Python语言中,for循环非常强大,乃至于通常都不怎么提倡使用递归,所有遇到递归的时候,最好都改为for循环。对于初学者而言,for循环理解起来并不难,一般的入门读物中也都这么解释:

1
2
3
4
5
6
7
8
>>> lst = [0,1,2,3]
>>> for i in lst:
... print(i)
...
0
1
2
3

变量 i依次引用列表list中的每个元素。比如我在自己的两本书《Python大学实用教程》和《跟老齐学Python:轻松入门》中,都是用这种方法对for循环进行了说明。

但是——转折了,非常重要——这种解释仅仅是就表象上向初学者做的解释,并没有揭示for循环的内在运行机制

我在《Python大学实用教程》一书中,曾以下面的方式对for循环做了深入阐述(参阅190页):

从这里我们知道,在进行 for循环的时候,其实是将被循环的对象转换为了可迭代对象——注意这个转换,非常重要。转换了之后,for循环是怎么运行的?在书中并没有深入讲解,下面我们就此给予介绍。

首先说明,本文内容的最主要参考或者说根据,就是Python语言的官方文档:https://docs.python.org/3/reference/compound_stmts.html#for,在这里对`for`循环语句有非常详细的说明。

1
2
for_stmt ::=  "for" target_list "in" expression_list ":" suite
["else" ":" suite]

按照上述文档中的说明,对于前面的示例,将列表lst=[0,1,2,3]作为for循环语句中的expression_list,即将其转化为可迭代对象,并且只转化一次,不妨用iter_lst表示这个可迭代对象。然后就依次将这个可迭代对象的元素读入内存,并按照顺序,依次赋值给target_list。注意,不论target_list是什么,都是将所读入的可迭代对象匀速依次赋值。

用上面循环语句示例理解这段话,其分解动作如下:

  1. lst=[0,1,2,3]转换为可迭代对象,暂记作iter_lst
  2. 读入iter_lst的第一个元素0,并将它赋值给i(这里的i就对应着上面语法规则中的target_list
    1. 于是有:i=0
    2. pirnt(i),就打印出了0
  3. 读入iter_lst的第二个元素1,并将它赋值给i
    1. 于是有:i=1
    2. print(i),就打印出了1
  4. $$\cdots$$ ,按照上面的过程不断重复,直到最后一个元素4为止——因为for循环语句能够自动捕获迭代到最后一个元素之后的异常,所以,for循环能够在到达最后一个元素之后,结束循环。

如果用代码的方式表示上面的过程,可以这样:

1
>>> iter_lst=iter(lst)

用内置函数iter(),依据列表lst创建了一个可迭代对象iter_lst

1
2
3
4
>>> iter_lst=iter(lst)
>>> i = next(iter_lst)
>>> print(i)
0

这就完成了第一个循环。然后依次方式,向下循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 第二个循环
>>> i = next(iter_lst)
>>> print(i)
1
# 第三个循环
>>> i = next(iter_lst)
>>> print(i)
2
# 第四个循环
>>> i = next(iter_lst)
>>> print(i)
3
# 到最后一个元素后面,抛出异常
>>> i = next(iter_lst)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

上面的演示,如果连贯起来,就是for循环——貌似没有什么奇怪的。

下面就要见证奇迹了。

经过上述操作之后,列表lst并没有发生变化——好像是废话。

1
2
>>> lst
[0, 1, 2, 3]

勿要着急,伟大总是孕育在平凡之中。

1
2
3
4
5
6
>>> iter_lst=iter(lst)
>>> i = next(iter_lst)
>>> print(i)
0
>>> lst
[0, 1, 2, 3]

完成第一循环,跟前面一样,并且在此时,查看了一下列表lst,没有变化。但是,我在这里做一个操作:

1
2
3
>>> lst[1]=111
>>> lst
[0, 111, 2, 3]

此时,将列表中序号为1的元素值修改为111,即lst[1]=111。如果按照读取可迭代对象的顺序,按照原来的流程,是要读取第二个元素1了,但是,在读取之间,我将列表中的第二个元素修改为111,那么,如果再进行下面的操作:

1
>>> i = next(iter_lst)

读取了可迭代对象的第二个元素,并把它赋值给变量i,此时,它是1还是111呢?

看结果:

1
2
>>> print(i)
111

不是1,而是111。再详细循环,就跟前述过程一样了。

这说明,如果将列表lst转换为可迭代对象之后,这个可迭代对象中的元素是对lst中元素的引用,并不是在可迭代对象中建立一套新的对象。

理解了上面的道理,看下面的操作,是不是能够解释?

1
2
3
4
5
>>> a = ['python', 'java', 'c', 'rust']
>>> iter_a = iter(a)
>>> a[1] = next(iter_a)
>>> a
['python', 'python', 'c', 'rust']

关键的一句是a[1] = next(iter_a)next(iter_a)得到了迭代器对象的第一个元素'python',并且将它赋值给a[1],这样,列表a中的索引是1的元素就变成了'python',即原来的'java'被替换为'python'了。

1
2
3
>>> a[1] = next(iter_a)
>>> a
['python', 'python', 'c', 'rust']

继续读取可迭代对象的第二个元素'python'

1
2
3
>>> a[1] = next(iter_a)
>>> a
['python', 'c', 'c', 'rust']

继续读取可迭代对象的第三个元素'c',在赋值给a[1],也就是列表a中的索引是1的元素变成了'c'

1
2
3
>>> a[1] = next(iter_a)
>>> a
['python', 'rust', 'c', 'rust']

这次a[1]='rust',根据前面的说明,应该容易理解了。

1
2
3
4
>>> a[1] = next(iter_a)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

最后报异常了。如果将上述过程,写成for循环,是这样的:

1
2
3
4
5
6
7
8
>>> a = ['python', 'java', 'c', 'rust']
>>> for a[1] in a:
... print(a, a[1])
...
(['python', 'python', 'c', 'rust'], 'python')
(['python', 'python', 'c', 'rust'], 'python')
(['python', 'c', 'c', 'rust'], 'c')
(['python', 'rust', 'c', 'rust'], 'rust')

上面循环语句中的a[1]就如同前面演示的i那样,都是循环语法结构中的target_list,只不过这里出了要完成赋值之外,还要同时实现对列表a中索引是1的元素修改,即实现上面分解动作中a[1] = next(iter_a)

似乎这里使用a[1]有点怪异。的确,在通常操作中很少这么做的。不过,上面的做法,倒是能让我们对for循环有了深刻理解。

理解了本文所介绍的内容,就不难回答stackoverflow上的一个问题了(https://stackoverflow.com/questions/55644201/why-can-i-use-a-list-index-as-an-indexing-variable-in-a-for-loop):

1
2
3
4
5
6
7
8
>>> b = [0,1,2,3]
>>> for b[-1] in b:
... print(b[-1])
...
0
1
2
2

是否能自己解释这个结果?

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

关注微信公众号,读文章、听课程,提升技能