深入理解for循环
2021-02-09
在Python语言中,for
循环非常强大,乃至于通常都不怎么提倡使用递归,所有遇到递归的时候,最好都改为for
循环。对于初学者而言,for
循环理解起来并不难,一般的入门读物中也都这么解释:
1 | 0,1,2,3] lst = [ |
变量 i
依次引用列表list
中的每个元素。比如我在自己的两本书《Python大学实用教程》和《跟老齐学Python:轻松入门》中,都是用这种方法对for
循环进行了说明。
但是——转折了,非常重要——这种解释仅仅是就表象上向初学者做的解释,并没有揭示for
循环的内在运行机制。
我在《Python大学实用教程》一书中,曾以下面的方式对for
循环做了深入阐述(参阅190页):
从这里我们知道,在进行 for
循环的时候,其实是将被循环的对象转换为了可迭代对象——注意这个转换,非常重要。转换了之后,for
循环是怎么运行的?在书中并没有深入讲解,下面我们就此给予介绍。
首先说明,本文内容的最主要参考或者说根据,就是Python语言的官方文档:https://docs.python.org/3/reference/compound_stmts.html#for,在这里对`for`循环语句有非常详细的说明。
1 | for_stmt ::= "for" target_list "in" expression_list ":" suite |
按照上述文档中的说明,对于前面的示例,将列表lst=[0,1,2,3]
作为for
循环语句中的expression_list
,即将其转化为可迭代对象,并且只转化一次,不妨用iter_lst
表示这个可迭代对象。然后就依次将这个可迭代对象的元素读入内存,并按照顺序,依次赋值给target_list
。注意,不论target_list
是什么,都是将所读入的可迭代对象匀速依次赋值。
用上面循环语句示例理解这段话,其分解动作如下:
- 将
lst=[0,1,2,3]
转换为可迭代对象,暂记作iter_lst
。 - 读入
iter_lst
的第一个元素0
,并将它赋值给i
(这里的i
就对应着上面语法规则中的target_list
)- 于是有:
i=0
pirnt(i)
,就打印出了0
- 于是有:
- 读入
iter_lst
的第二个元素1
,并将它赋值给i
- 于是有:
i=1
print(i)
,就打印出了1
- 于是有:
- $$\cdots$$ ,按照上面的过程不断重复,直到最后一个元素
4
为止——因为for
循环语句能够自动捕获迭代到最后一个元素之后的异常,所以,for
循环能够在到达最后一个元素之后,结束循环。
如果用代码的方式表示上面的过程,可以这样:
1 | iter_lst=iter(lst) |
用内置函数iter()
,依据列表lst
创建了一个可迭代对象iter_lst
。
1 | iter_lst=iter(lst) |
这就完成了第一个循环。然后依次方式,向下循环:
1 | # 第二个循环 |
上面的演示,如果连贯起来,就是for
循环——貌似没有什么奇怪的。
下面就要见证奇迹了。
经过上述操作之后,列表lst
并没有发生变化——好像是废话。
1 | lst |
勿要着急,伟大总是孕育在平凡之中。
1 | iter_lst=iter(lst) |
完成第一循环,跟前面一样,并且在此时,查看了一下列表lst
,没有变化。但是,我在这里做一个操作:
1 | 1]=111 lst[ |
此时,将列表中序号为1
的元素值修改为111
,即lst[1]=111
。如果按照读取可迭代对象的顺序,按照原来的流程,是要读取第二个元素1
了,但是,在读取之间,我将列表中的第二个元素修改为111
,那么,如果再进行下面的操作:
1 | i = next(iter_lst) |
读取了可迭代对象的第二个元素,并把它赋值给变量i
,此时,它是1
还是111
呢?
看结果:
1 | print(i) |
不是1
,而是111
。再详细循环,就跟前述过程一样了。
这说明,如果将列表lst
转换为可迭代对象之后,这个可迭代对象中的元素是对lst
中元素的引用,并不是在可迭代对象中建立一套新的对象。
理解了上面的道理,看下面的操作,是不是能够解释?
1 | 'python', 'java', 'c', 'rust'] a = [ |
关键的一句是a[1] = next(iter_a)
。next(iter_a)
得到了迭代器对象的第一个元素'python'
,并且将它赋值给a[1]
,这样,列表a
中的索引是1
的元素就变成了'python'
,即原来的'java'
被替换为'python'
了。
1 | 1] = next(iter_a) a[ |
继续读取可迭代对象的第二个元素'python'
。
1 | 1] = next(iter_a) a[ |
继续读取可迭代对象的第三个元素'c'
,在赋值给a[1]
,也就是列表a
中的索引是1
的元素变成了'c'
。
1 | 1] = next(iter_a) a[ |
这次a[1]='rust'
,根据前面的说明,应该容易理解了。
1 | 1] = next(iter_a) a[ |
最后报异常了。如果将上述过程,写成for
循环,是这样的:
1 | 'python', 'java', 'c', 'rust'] a = [ |
上面循环语句中的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 | 0,1,2,3] b = [ |
是否能自己解释这个结果?
若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏
关注微信公众号,读文章、听课程,提升技能