深入理解for循环
2021-02-09
在Python语言中,for循环非常强大,乃至于通常都不怎么提倡使用递归,所有遇到递归的时候,最好都改为for循环。对于初学者而言,for循环理解起来并不难,一般的入门读物中也都这么解释:
| 1 | lst = [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 | 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 | lst[1]=111 | 
此时,将列表中序号为1的元素值修改为111,即lst[1]=111。如果按照读取可迭代对象的顺序,按照原来的流程,是要读取第二个元素1了,但是,在读取之间,我将列表中的第二个元素修改为111,那么,如果再进行下面的操作:
| 1 | i = next(iter_lst) | 
读取了可迭代对象的第二个元素,并把它赋值给变量i,此时,它是1还是111呢?
看结果:
| 1 | print(i) | 
不是1,而是111。再详细循环,就跟前述过程一样了。
这说明,如果将列表lst转换为可迭代对象之后,这个可迭代对象中的元素是对lst中元素的引用,并不是在可迭代对象中建立一套新的对象。
理解了上面的道理,看下面的操作,是不是能够解释?
| 1 | a = ['python', 'java', 'c', 'rust'] | 
关键的一句是a[1] = next(iter_a)。next(iter_a)得到了迭代器对象的第一个元素'python',并且将它赋值给a[1],这样,列表a中的索引是1的元素就变成了'python',即原来的'java'被替换为'python'了。
| 1 | a[1] = next(iter_a) | 
继续读取可迭代对象的第二个元素'python'。
| 1 | a[1] = next(iter_a) | 
继续读取可迭代对象的第三个元素'c',在赋值给a[1],也就是列表a中的索引是1的元素变成了'c'。
| 1 | a[1] = next(iter_a) | 
这次a[1]='rust',根据前面的说明,应该容易理解了。
| 1 | a[1] = next(iter_a) | 
最后报异常了。如果将上述过程,写成for循环,是这样的:
| 1 | a = ['python', 'java', 'c', '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 | b = [0,1,2,3] | 
是否能自己解释这个结果?
若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏
 
        关注微信公众号,读文章、听课程,提升技能
