Python入门第六课:for循环、range()与数字列表实战
1. 第六课为什么把 for 循环单独拎出来讲
带入门班带到第六课,几乎每期都会遇到同一个分水岭:前五节课学员写的代码,行数基本等于手工劳动量,三个数排序写三行比较,五个成绩求平均写五行赋值求和。到了第六课,变量、列表、条件判断都认得全了,此时如果还不给 for 循环,学员面对“求 1 到 100 的和”这种题就只能硬算,或者干脆复制粘贴一百行。for 循环和数字列表这一课,本质上是把读者从“手工时代”推进“批处理时代”的钥匙:一次写好规则,剩下交给解释器去重复。而 range() 函数、list(range()) 生成数字列表、min/max/sum 统计、列表推导式,这四个东西一旦打通,后面文件处理、数据清洗、爬虫解析、报表生成才有得谈。这一课适合两类人:一类是刚看完列表、现在卡在“怎么让代码自己动起来”的零基础读者;另一类是学过一点循环但每次写都靠猜边界、靠试运行的半新不旧的人。
1.1 从“复制粘贴三行代码”到“一次处理一万条”
先说清楚这个痛点有多真实。我给学员布置过一个很小的题:把一份十个人的成绩逐个加 5 分。前十份提交里,有六份是这么写的——score1 = score1 + 5、score2 = score2 + 5……一直排到score10。代码能跑,结果也对,问题在于这十行没有任何“结构”,它只是十行。如果名单变成一百个人,作者就会崩。for 循环解决的不是“省几个字”的问题,而是把“对每一个元素做同一件事”这件事本身写成代码。思维上要从“我要处理第 1 个、第 2 个……”切换到“我要描述一条作用于任意元素的规则”。这个切换听着抽象,落到纸面上就是一句话:循环体里只写一次,外面用 for 圈定范围。
我在课上会做个小演示:写十行赋值版本,然后当着面改成三行循环,再改成一行列表推导式。学员通常会被最后一步惊到,但我会压住节奏——推导式是一层语法糖,糖底下的循环才是理解的主体,先能徒手写 for,再谈一行流。这个顺序错了,后面遇到循环里嵌条件、循环里改数据就会抓瞎。
1.2 for 和 while 到底怎么选
新手很常见的困惑是:既然 while 也能循环,为什么还要 for?这两个不是替代关系,是分工关系。我的判断标准很粗暴:次数或集合是否已知。已知一个容器要逐个过一遍,或者知道要重复多少次,用 for;只知道“直到某个条件不成立才停”,用 while。求 1 到 100 的和,次数是 100,用 for;让用户不停输入密码直到正确,次数不确定,用 while。用 while 去数列表元素是典型的错配,它会把“计数变量自增”“边界判断”“越界风险”三件事全塞给你,而 for 自动帮你做完了。
| 对比维度 | for 循环 | while 循环 |
|---|---|---|
| 驱动因素 | 可迭代对象里的元素,或 range 给出的次数 | 一个布尔条件 |
| 循环变量 | 自动赋值,每次取一个元素 | 需要自己定义、自己更新 |
| 越界风险 | 基本没有,走完就停 | 有,忘记自增或判断写反就死循环 |
| 典型场景 | 遍历列表、字符串、数字范围 | 交互式输入、条件不定的重试 |
| 新手容易出错的地方 | 缩进、边界差一 | 忘记更新变量导致卡死 |
提示:如果你写 while 的循环体里没有出现任何“会让条件变假”的语句,先停下来检查,这大概率是死循环。
1.3 我安排这节课的顺序
顺序不是随便排的,它对应认知的递进。先讲语法骨架,是因为 for 的行、冒号、缩进这三样东西必须固化成肌肉记忆,否则后面所有内容都建在沙子上。再讲 range,是因为数字列表的产生方式必须先落地,学员得先能“造出 1 到 10”这堆数据,才谈得上处理。接着讲数字列表的统计与切片,把循环的结果真正用起来。然后是推导式,作为熟练后的简化。最后一定是坑和排查,这一节我从不砍,因为第六课之后的作业难度会陡增,没这一节,学员会在缩进和边界上反复摔跤,浪费两周时间。
2. for 循环的语法骨架:四个元素缺一个都不行
2.1 for、in、冒号、缩进这四件套
一个完整的 for 语句长这样:
scores = [78, 92, 65, 88] for score in scores: print(score)四样东西必须齐全:关键字for、循环变量score、关键字in加被遍历的对象,行尾的英文冒号,以及下一行的缩进。少了冒号报SyntaxError,缩进没对齐报IndentationError,这两个错误占了新手报错总量的一大半。我常跟学员说,冒号的意思是“下面开始是我的地盘”,缩进就是拿尺子划出来的地界,地界之外的东西循环管不着。
这里有个很关键的机制要讲透:循环体里的代码会执行多少次、在什么时机执行,取决于缩进层级。看下面这段:
total = 0 for n in [1, 2, 3]: total = total + n print("累计:", total) print("循环结束,最终结果:", total)前三行在循环里,每轮都跑;最后一行缩进退回来了,只在循环结束后跑一次。很多人的 bug 就出在这里——本该放循环外的语句缩进去了,于是一百次循环打印了一百遍;本该放循环里的语句退出来了,于是只算了最后一个元素。
2.2 可迭代对象:一摞等着被拿走的盘子
in后面能放什么?放“可以被一个个取出来的东西”。列表、字符串、range 对象、字典的键、文件的每一行,都属于这一类。我喜欢用一摞盘子打比方:for 循环就是伸手拿盘子,拿一个处理一个,拿完为止,你不需要提前知道有几个。字符串也能遍历,这常被忽略:
for ch in "python": print(ch)会逐字符打印。这个特性在后面做文本统计(数某个字出现几次)时非常顺手。反过来说,整数、浮点数不能直接放在 in 后面,写for x in 5会报TypeError: 'int' object is not iterable——“不可迭代”这个报错,翻译成人话就是“这东西没法一个个取出来”。遇到这个报错,第一反应是检查 in 后面是不是忘了套 range 或者 list。
底层一点说,for 循环第一步会对目标调用一次迭代协议,拿到一个迭代器,之后每轮问它“还有下一个吗”。这套机制对使用者是透明的,但理解它有个好处:同一个列表可以遍历多次,而迭代器被消费完就空了。有些学员用生成器做完一次统计后想再遍历一遍,结果什么都没打印,就是踩在这里。
2.3 循环变量怎么命名,enumerate 什么时候上
循环变量名不影响运行,但影响三个月后你还认不认得这段代码。for i in scores能跑,可读性差;for score in scores一眼就懂。列表里装的是成绩就用 score,是名字就用 name,是商品就用 item。这条不是洁癖,是给未来的自己留说明书。
当需要同时拿到序号和元素时,别自己维护计数器,用enumerate:
names = ["A同学", "B同学", "C同学"] for index, name in enumerate(names, start=1): print(f"第{index}位:{name}")start=1让编号从 1 开始,符合人的阅读习惯。很多新手会写i = 0然后在循环里i += 1,能跑,但多了一个需要维护的变量,就多了一个出错点。同样的道理,两个列表一一配对遍历用zip,比按下标去索引两个列表干净得多,也避免了长度不一致时越界。
3. range() 的三种写法和边界到底怎么算
3.1 三种形态,一张表说清
range 是“生成一串整数”的工厂,三种调用方式覆盖了绝大多数需求:
| 写法 | 含义 | 生成的序列 | 常见用途 |
|---|---|---|---|
range(5) | 从 0 开始,到 5 之前 | 0,1,2,3,4 | 控制重复次数 |
range(1, 6) | 从 1 开始,到 6 之前 | 1,2,3,4,5 | 计数从 1 起的场景 |
range(0, 10, 2) | 从 0 开始,步长 2 | 0,2,4,6,8 | 隔一个取一个 |
range(5, 0, -1) | 从 5 开始倒着走 | 5,4,3,2,1 | 倒序处理 |
其中最容易翻车的就是第二种。想要 1 到 5,必须写range(1, 6)。我每期都会有人在作业里写range(1, 5)然后疑惑为什么少了 5。
3.2 左闭右开怎么记:别背,画数轴
我从不要求学员背“左闭右开”这个术语,只教一个动作:在纸上画一条数轴,把两个数字标上去,然后把右端点那个数圈起来打个叉,叉左边那一格才是最后一个。range(1, 6),1 和 6 标出来,6 打叉,最后一个就是 5。这个动作做十遍,肌肉记忆就有了。
换个更好记的说法:range 的两个参数其实是“起点”和“终点外一格”,就像量身高时说“从 0 到 180 厘米”包含了 179.9,但标记线画在 180。或者用排队类比:range(1, 6)是“1 号站到位,到 6 号位置前一位为止”。
再补一条硬算公式:序列长度等于 (stop - start) 除以 step 后向上取整。比如range(2, 11, 3),跨度 9,除以 3 得 3,元素是 2、5、8,正好三个。会这个公式,遇到“步长不是 1”的情况不会慌。
3.3 负步长倒着数
倒序有两种写法,各有适用面。range(5, 0, -1)直接产出 5 到 1;如果手里已经有列表,用切片[::-1]更省事。注意负步长时,起点必须大于终点,否则得到空序列——range(1, 5, -1)什么都不产出,还不报错,这是最阴的一类 bug,因为它静悄悄地什么也不做。我一般建议在写负步长的循环后,先单独print(list(range(...)))看一眼结果再往下写。
3.4 range 对象不是列表
Python 3 里range(1000000)瞬间返回,不占内存,因为它并不真的把一百万个数字造出来,而是“你问我要第几个,我现算”。这叫惰性。好处是省内存,代价是它不支持切片、不能直接相加、打印出来是range(0, 5)而不是[0, 1, 2, 3, 4]。想看到内容,套一层 list。想求长度用len(range(1, 6))得到 5,判断某个数在不在里面用3 in range(1, 6)得到 True。
注意:如果你是从很老的教程里学的 Python,可能会看到“range 返回列表”的说法,那是 Python 2 的遗留。判断标准很简单,
print(range(5))出来带方括号就是列表,带 range 字样就是对象。
4. 数字列表:生成、统计、切片一条龙
4.1 用 list(range()) 造一串数字
数字列表就是元素全是数字的列表,最常见的造法是list(range(...)):
numbers = list(range(1, 11)) print(numbers) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]注意外面那层list()不能省。range(1, 11)只是“描述了一个范围”,套上 list 才是“把范围内的数真的装进列表”。这一步的动机很实际:range 只能生成等差数列,而列表能装任意数字,能改、能加、能排序、能切片。
另一条路是先用循环加 append 一点点往里塞:
cubes = [] for n in range(1, 6): cubes.append(n ** 3)这种“先建空列表、再逐个追加”的写法是循环与列表结合的经典范式,值得练熟,因为后面处理文件、爬数据基本都是这个套路。
4.2 min、max、sum 与手算统计
有了数字列表,三个内置函数直接给出极值和总和:
data = [88, 76, 95, 60, 92] print(min(data)) # 60 print(max(data)) # 95 print(sum(data)) # 411 print(sum(data) / len(data)) # 82.2求平均值必须手动写sum/len,Python 没有内置的 mean,很多人第一次会去找data.mean()然后报错——列表没有这个方法,那是别的数据处理库的写法。平均值这里有个坑:空列表求平均值会触发除零错误,如果数据来自文件读取,遇到空文件就炸。稳妥的做法是先判空。
再往上一层,用循环做“带条件的统计”,比如数出及格人数:
passed = 0 for s in data: if s >= 60: passed += 1 print("及格人数:", passed)这段代码里有三样东西值得强调:计数器passed必须在循环外初始化为 0,否则第一轮就报变量未定义;+=是passed = passed + 1的简写;缩进层级决定if在不在循环内。学员出错最多的不是逻辑想不明白,而是把passed = 0写进了循环体,于是每轮都清零,最后永远是 1。这个错误我见过至少二十次。
4.3 列表推导式:把三行压成一行
上一节那段算立方的循环,可以写成:
cubes = [n ** 3 for n in range(1, 6)]读法是“对 range(1,6) 里的每个 n,取出 n 的立方,组成一个列表”。顺序记法:把普通 for 循环的三行代码竖着叠——第一行是结果表达式n ** 3,第二行是 for,第三行放在最前面,就变成了推导式。加上条件也简单,[n ** 2 for n in range(1, 11) if n % 2 == 0]得到偶数的平方。
推导式好用,但我有一个明确的劝退边界:超过一层 for、超过一个 if 就别用。见过学员写出带两个嵌套循环加条件的推导式,一行占了屏幕大半,出了 bug 根本没法插 print 调试。这种时候老老实实展开成循环,可读性和可维护性都更高。推导式是给人看的,不是给机器炫技的。
4.4 切片与步长切片
切片是列表的“截取”,语法列表[起点:终点:步长],同样是左闭右开:
| 写法 | 结果(以 1 到 10 为例) | 说明 |
|---|---|---|
nums[:3] | [1,2,3] | 前三个 |
nums[-3:] | [8,9,10] | 后三个 |
nums[2:5] | [3,4,5] | 下标 2 到 4 |
nums[::2] | [1,3,5,7,9] | 隔一个取 |
nums[::-1] | [10,...,1] | 反转 |
起点省了默认从头,终点省了默认到尾,三个都省就是整个列表的副本。切片不改变原列表,这跟sort()和sorted()的区别一样,是新手很爱混的点。要记住:切片是复印,不是撕原件。
4.5 两个马上能用上的小案例
第一个,批量单位换算。一份以摄氏度记录的温度列表,转成华氏度:
celsius = [12, 18, 25, 31, 7] fahrenheit = [c * 9 / 5 + 32 for c in celsius] print(fahrenheit)第二个,从一长串数字里挑出符合区间的部分:
raw = [1024, 87, 5000, 320, 12, 800] valid = [x for x in raw if 100 <= x <= 1000] print(valid) # [320, 800] print(sum(valid)) # 1120这两个例子都不是玩具,前者是数据清洗里的标准化,后者是过滤异常值。学员做完这两个,普遍会有一个“原来循环是干这个用的”的顿悟时刻,比讲十遍概念管用。
5. 新手最常撞的六个坑和排查表
5.1 缩进类:Tab 和空格混着用
Python 靠缩进划分代码块,所以缩进错误不是“格式问题”,是“语法错误”。最常见的两种情况:一是同一层级用了 Tab 又用了空格,肉眼看对齐了,解释器认为不对齐,报TabError或IndentationError;二是循环体忘了缩进,print顶格写,结果只有最后一行被执行。
解决办法很土但有效:统一用四个空格,编辑器里设置“按 Tab 插入空格”。另外,冒号打完回车,编辑器一般会自动缩进,不要手动再按一次空格去“微调”,越调越乱。
5.2 边界类:差一错误
需求写“打印 1 到 10”,写成range(1, 10)只到 9。需求写“列表最后一个元素”,写成nums[len(nums)]直接越界,正确是nums[-1]或nums[len(nums) - 1]。这类错误的特点是不报错或报得很远,逻辑错了但程序照跑。我的建议是养成一个小动作:涉及 range 的地方,写完后立刻在旁边打印一次list(range(...))或者它的长度,用眼睛验一遍。三十秒的成本,省掉半小时的排查。
5.3 逻辑类:遍历时改列表、累加器没初始化
遍历列表的同时删除元素,是入门阶段最经典的陷阱:
nums = [1, 2, 3, 4, 5, 6] for n in nums: if n % 2 == 0: nums.remove(n)结果不是想象的只剩奇数,而是漏删了元素。原因在于遍历是按位置推进的,删掉一个元素后后面的整体前移,循环指针也跟着走,就跳过了一个。稳妥做法是用推导式生成新列表[n for n in nums if n % 2 != 0],或者遍历副本for n in nums[:]。原则一句话:遍历谁,就别改谁。
另一个高频 bug 是累加器位置。求和、计数、拼字符串这三种操作,累加变量必须放在循环之前初始化,且初始化那一行不能缩进进循环。
5.4 类型类:range 不吃小数、字符串和数字混算
range(0, 1, 0.1)会直接报TypeError,因为 range 只接受整数。想要 0 到 1 之间的 0.1 步长,常见做法是用整数循环再除以 10:[i / 10 for i in range(0, 11)]。这个换算思路在绘图刻度、分档统计里很常用。
另一种类型坑是输入。从输入框拿到的东西是字符串,"5" + 1会报错,必须先转成整数。而列表里混着字符串和数字时,sum()也会直接崩。排查方法很简单:在出问题的地方print([type(x) for x in data]),一眼看出谁不是数字。
5.5 排查速查表
| 现象 | 最可能的原因 | 处理动作 |
|---|---|---|
IndentationError | 缩进不一致或该缩进没缩进 | 全选,统一转换 Tab 为空格 |
SyntaxError: invalid syntax | 行尾漏了冒号,或中文标点 | 检查 for 行末尾,切英文输入 |
TypeError: 'int' object is not iterable | in 后面直接放了数字 | 套上range()或list() |
| 结果比预期少一个 | range 右端点写成了目标值本身 | 右端点加 1 |
| 循环体只执行了一次 | 累加器或计数在循环内被重置 | 把初始化语句移到循环外 |
| 循环一次都不执行 / 死循环 | 负步长方向写反 / while 条件永真 | 打印 range 内容;检查条件更新 |
| 求和结果是个长字符串 | 列表里混了字符串没转换 | 逐元素int()或float() |
提示:报错信息里的行号是朋友不是敌人。Python 的报错通常准确指向出错那一行或它的上一行,先看最后一行报错类型,再往上找。
6. 带班实操:练习怎么排、代码怎么调
6.1 三道递进的练习题
第一道,纯 range 练手:打印 1 到 20 的所有偶数,再求它们的和。这道题把 range 边界、步长、累加三件事串起来,做对说明语法过关。第二道,数字列表统计:给一组任意成绩,输出最高分、最低分、平均分、及格率。这道题的坑在平均值和及格率的除零判断,以及计数器位置。第三道,综合应用:生成 1 到 30 的平方列表,只保留能被 3 整除的项,输出列表和它们的总和。这道题可以做两遍,一遍用普通 for 加 append,一遍用推导式,让学员自己感受两种写法的差异,再讨论什么时候该用哪种。
我特意把题量控制在三道,因为第六课的重点是建立“循环思维”,不是刷题量。三道题各做一个变体,比囫囵做完十道有用得多。批改时我只看两件事:累加变量的位置对不对,边界有没有差一。这两处对了,其余小毛病都好纠。
6.2 调试习惯:小数据先跑通
学员最常犯的调试错误是拿真实数据直接开跑,比如一万行的成绩单,出错后无从下手。我的建议固定成三步。第一步,把数据换成三五条手写的假数据,比如[90, 60, 75],先确认逻辑跑通。第二步,在循环体里加一行print,把每轮的循环变量和累加结果打出来,肉眼看走向是否符合预期。第三步,确认无误后再换回真实数据。这三步多花两分钟,能省掉大量猜测。
还有个小技巧值得说:把循环里的中间结果打印成“第几轮、当前元素、当前累计”三列格式,比只打印一个数字有用得多。比如print(f"第{i}轮 元素{n} 累计{total}"),出问题时一眼看出是从哪一轮开始偏的。
6.3 几个绕不开的经验
第一个经验关于列表推导式的使用节奏。我见过太多人在刚学会推导式后,把所有循环都改成一行流,包括那些带两三层判断、逻辑本来就复杂的代码。三个月后回头看,自己都读不懂。我的判断标准是:能一眼读完且不需要注释就懂,用推导式;否则老实展开。推导式的价值在于简化简单的循环,不在于压缩复杂的逻辑。
第二个经验关于数字列表的范围控制。生成 1 到 100 万这种大列表在 Python 里是可行的,但要清楚它的内存成本——每个整数对象都有开销,一百万个数不是一百万字节。真要处理大规模序列,用 range 直接迭代就够了,没必要先list()出来再遍历。这个区别在数据量小的时候完全看不出,一旦上了规模就是几倍的差距。
第三个经验关于代码的可读性投入。第六课之后,学员的代码开始从十几行涨到几十行,这时候变量命名、空行分组、适当注释的价值才开始显现。我会在这个阶段强制要求循环变量用有意义的名字,累加变量叫total而不是t。这不是形式主义,是因为调试的时候你会反复扫这段代码,每一个含义模糊的名字都在消耗你的注意力。
我自己带课这么些年,最深的一个体会是:for 循环这一课的难点从来不在语法,语法半小时就能讲完。难在学员要接受一种新的思考方式——不再描述“第一步做什么、第二步做什么”,而是描述“对每一个元素做什么”。这个转换有的人当场就通了,有的人要写坏三四个程序才转过弯。所以这一课我从不赶进度,宁可多留两道练习题,多花半小时逐个看作业里的缩进和边界。基础阶段省下的时间,后面都要以加倍的调试时间还回来。