Python入门第六课:for循环、range()与数字列表实战

发布时间:2026/11/3 7:18:43
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 开始,步长 20,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 iterablein 后面直接放了数字套上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 循环这一课的难点从来不在语法,语法半小时就能讲完。难在学员要接受一种新的思考方式——不再描述“第一步做什么、第二步做什么”,而是描述“对每一个元素做什么”。这个转换有的人当场就通了,有的人要写坏三四个程序才转过弯。所以这一课我从不赶进度,宁可多留两道练习题,多花半小时逐个看作业里的缩进和边界。基础阶段省下的时间,后面都要以加倍的调试时间还回来。