ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python for循环与数字列表:range、遍历与统计入门

Python for循环与数字列表:range、遍历与统计入门

从“抄一遍”到“想明白”:Python 第六课里的 for 循环和数字列表

带过几轮零基础学员之后我发现一个规律:前五课大家学得都挺顺,变量、字符串、列表、条件判断,照着敲都能跑出结果,可一到第六课Python 中的 for 循环及数字列表,进度明显开始分化。有人当天就能用循环把一整个班的信息统计出来,有人盯着for i in range(5):反复问“这个 i 到底从哪来、到哪去”。这不是悟性问题,而是这一课第一次把“写一行执行一行”的线性思维,切换成了“让机器替我重复”的批处理思维,脑子没转过来,代码怎么写都别扭。

for 循环在 Python 里的地位,相当于手工活里的“模具”:你把一次动作定义好,剩下的事情交给它重复执行。而数字列表是这套模具最顺手的原料——一堆规整的整数、一串待统计的数值、一段需要按序号处理的区间。第六课把这两样东西放在一起讲,其实是在搭一条最小的自动化流水线:用range()造数据,用for走一遍数据,用sum()、min()、max()、len()出结果。

这篇内容适合三类人看:刚学完列表和条件判断、准备啃循环的新手;自学时被缩进和边界值反复折磨的转行者;以及需要带别人入门、想找一套讲得清楚又不啰嗦的讲法的朋友。下面我按自己实际讲课和踩坑的顺序来展开,代码都能直接复制运行,参数怎么来的也会一步步推导给你看。

1. 为什么第六课要卡在 for 循环这个位置

1.1 课程的编排顺序其实藏着一套依赖关系

先说清楚一件事:教材把 for 循环放在第六课,不是随意排的,而是前面的知识点已经凑齐了触发循环的三个前置条件。变量教会你给数据起名字,列表教会你把一堆数据装进一个容器,条件判断教会程序根据情况走不同分支。这三样东西合起来,才构成 for 循环的最小运行环境——你得有个容器可以被遍历,得有个变量去接住每次取出来的元素,还得有判断逻辑来决定循环体里做什么。

如果顺序反过来,先讲循环再讲列表,学员会遇到一个很尴尬的局面:for后面没东西可遍历,只能拿字符串凑数,讲不深也用不上。等到学列表的时候又要回头补循环,知识是碎的。所以这一课真正的定位是“第一次让程序处理批量数据”,理解了这一点,后面学字典遍历、文件逐行读取、爬虫翻页,全都是同一个套路换个容器而已。

我个人的判断标准很朴素:一个人是不是真的过了第六课这一关,不看他会不会背语法,而是看他遇到“要对一百条数据做同一件事”时,第一反应是写一百行还是写一个循环。前者说明还在用电子表格的思维写代码,后者才算入门。

1.2 for 循环和 while 循环到底该选哪个

新手最常见的困惑是:既然有了 while,为什么还要 for?这两个东西表面都能重复执行,骨子里的适用场景完全不同。

我的经验是把它们分成两类:知道要跑多少次,用 for;只知道什么时候该停,用 while。遍历一个列表、生成 1 到 100 的数、把字符串每个字符过一遍,次数是确定的或者由容器长度决定的,这种活交给 for,代码短、不容易写出死循环。而“一直读取输入直到用户输入 q 为止”“不断重试直到成功”这类场景,你事先不知道会转几圈,只能靠条件卡住,那就用 while。

还有一种情况要特别提醒:很多人用 while 写计数循环,写成这样——i = 0,然后while i < 10:,循环体末尾手动i += 1。这段代码本身没错,但只要某条分支提前continue之前忘了加一,立刻死循环。for 循环天生把这个计数器交给你管,for i in range(10):里i的推进由解释器负责,你根本没机会写漏。这就是我为什么坚持让学员在能确定次数的情况下优先用 for——少一个可以写错的地方,就少一个半夜排查 bug 的理由。

1.3 数字列表为什么非要和循环绑在一起讲

单独讲列表,学员只能做增删改查;单独讲循环,学员只能打印字符串。两者一结合,才出现真正有价值的东西:统计。求总分、算平均分、找最高分、数有多少个及格的——这些操作在现实中出现的频率远高于“把列表排序打印出来”。

而且数字列表有个天然优势:它规整。list(range(1, 11))出来就是十个连续的整数,你在验证循环逻辑对不对的时候,心里能默算出正确答案。如果换成字符串或者混合类型,一旦输出不对,你都不确定是循环写错了还是数据本身有问题。用规整的数据调试逻辑,用真实的数据验证功能,这个习惯我从第六课就开始让学员建立。

2. for 循环的机制拆解:把它当成“挨个取件”

2.1 遍历的本质是“取出一个、执行一遍、再取下一个”

理解 for 循环,最好的类比是快递驿站取件。货架上一排包裹(这就是列表),工作人员从第一个开始,取一个、扫码登记(执行循环体)、放一边,再取下一个,直到货架空掉为止。for关键字后面那个变量,就是“手上正拿着的那个包裹”。

fruits = ["苹果", "香蕉", "橙子"] for fruit in fruits: print(f"正在处理:{fruit}")

输出三行,顺序和列表里一致。这里有个新手常问的问题:fruit这个名字是谁规定的?答案是随便起,它只是临时的名字,叫x也行、叫item也行。但我建议起有意义的名字——fruit一看就知道装的是水果,x过三天你自己都想不起来它代表什么。变量名是写给人看的,不是写给解释器看的,这句话我在课上重复过很多次。

再补一个容易被忽略的细节:循环结束后,fruit这个变量并不会消失,它还留在内存里,值是最后一个被取出的元素。这在 Python 里是允许的,虽然很多其他语言里循环变量出了循环就作废。所以千万别在循环后面复用同名变量,尤其是你打算拿它存别的东西的时候,很容易出莫名其妙的错。

2.2 冒号和缩进:Python 用空白字符表达结构

第一次看到 Python 代码的人都会愣一下:怎么没有大括号?答案是 Python 用缩进来表达“谁属于谁”。冒号:宣告“下面开始一个代码块”,紧接着缩进的部分就是块内部,缩进退回来就代表块结束。

total = 0 for n in [1, 2, 3, 4]: total = total + n print("当前累计:", total) print("循环结束,总分是:", total)

前两条print和total都在缩进内,每轮都执行;最后一条退回到顶格,循环全部跑完才执行一次。

注意:缩进必须统一,全程 4 个空格最稳妥。全空格和全 Tab 混着用会直接报TabError: inconsistent use of tabs and spaces in indentation,这个报错在编辑器里肉眼几乎看不出来,尤其是从网页上复制代码过来的时候,最常见的坑就是这个。

我一般让学员把编辑器的“显示空白字符”打开,Tab 会显示成箭头,空格显示成点,一眼就能看出混没混。另外,忘了写冒号报的是SyntaxError: expected ':',而写了冒号没缩进报的是IndentationError: expected an indented block,这两种错误信息都算友好,照着改就行,别慌。

2.3 break、continue 和 for-else 这三件工具

循环体里偶尔会出现“提前收工”或者“跳过这一轮”的需求,Python 给了两个关键字。

break是直接砸门走人,整个循环立刻结束,后面的元素一个都不看了。典型场景是查找:列表里找到目标值就可以停了,没必要把剩下的全走一遍。continue是这一轮不干了,但还没下班,直接跳到下一轮从头开始。典型场景是过滤:遇到偶数就跳过,只处理奇数。

numbers = [3, 8, 15, 22, 7] for n in numbers: if n % 2 == 0: continue # 偶数跳过,不往下执行 if n > 20: break # 超过 20 就整体收工 print("处理奇数:", n)

至于for...else,是 Python 特有的一种写法:循环正常跑完(没被 break 打断)就执行 else 块,被 break 打断就不执行。新手看到会觉得别扭,硬记语法没什么意义,等真的遇到“找了一圈没找到就报个提示”的需求时再回来看,一下就懂了。

3. range() 的三种形态与边界值推导

3.1 三种写法对应三种需求

range()是造数字序列的工厂,一共三种调用方式,都是从简单到复杂排列的。

写法含义示例结果典型用途
range(stop)从 0 开始,到 stop 前停range(5)→ 0,1,2,3,4按序号遍历、固定次数重复
range(start, stop)从 start 开始,到 stop 前停range(1, 6)→ 1,2,3,4,5从 1 开始计数
range(start, stop, step)加步长range(0, 10, 3)→ 0,3,6,9隔行处理、倒序

注意range()返回的不是列表,而是一个“惰性”的对象,你直接print(range(5))看到的会是range(0, 5)而不是[0, 1, 2, 3, 4]。想看具体内容得用list()转一下。这个设计是为了省内存——就算写range(100000000),它也只是一句话,不会真在内存里铺一亿个数。

3.2 为什么 range(1, 5) 只到 4

“左闭右开”是新手第一周最容易翻车的地方。range(1, 5)生成的是 1、2、3、4,不含 5。为什么这么设计?一句话解释:便于用“长度”直接当右边界。如果我要遍历一个长度是 5 的列表,直接写range(len(lst))就是 0 到 4,正好是合法索引,不用再减一。要是右边界包含,所有涉及索引的循环都得写len(lst) - 1,多一个减号就多一处写错的可能。

实操中最容易出错的场景是“求 1 到 100 的和”。很多人直觉写range(1, 100),结果少加了 100,答案是 4950 而不是 5050,差了整整一百。验证方法很简单,拿小数据试:list(range(1, 5))跑一下,看到没有 5,自然就知道要写 101 了。

提示:凡是写range(),先用小范围跑一遍list()看输出,确认边界对了再放到大循环里。这个习惯能省掉大量“结果差一点点”的排查时间。

3.3 步长为负和“浮点数不可用”这两条铁律

倒序生成数字,得靠负步长:range(10, 0, -1)出来是 10 到 1。这里有两个坑要提前说清楚。

第一,步长为负时,起始值必须大于结束值,否则什么都生成不出来,而且是静默返回空,不报错。range(1, 10, -1)的结果是空序列,循环体会一次都不执行。新手在这里经常懵:明明写了循环,怎么一行输出都没有?原因就在这里。

第二,range()只接受整数,写range(0, 1, 0.1)会直接抛TypeError。要生成 0 到 1 之间步长 0.1 的序列,常见做法是先算次数:[i / 10 for i in range(0, 11)],或者用整数序号乘以步长换算。这个知识点在第六课讲不一定用得上,但提前埋一句,等后面遇到画图坐标、物理模拟的时候就知道有多重要了。

关于“range 能生成多少个元素”,给个可以心算的公式,步长为正时:元素个数 =(stop - start + step - 1) // step。比如range(1, 10, 2),(10 - 1 + 2 - 1) // 2 = 5,实际就是 1、3、5、7、9,五个,对得上。这个公式在预估循环次数、判断性能时挺有用,比如我要循环一百万次,先算一下大概耗时再决定要不要优化。

4. 数字列表的生成、统计与推导式

4.1 用 list() 配合 range() 一次造一整套数据

list(range(n))是最常用的造数方式,一行就能拿到 0 到 n-1 的列表。

nums = list(range(1, 11)) print(nums) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

另一种写法是用循环逐个追加:

squares = [] for n in range(1, 11): squares.append(n ** 2) print(squares)

这两种方式的选择上,我一般建议学员先老老实实写第二种。为什么?因为 append 的写法暴露了“先建空列表、再往里塞”的过程,这个模式在真实开发里出现频率极高,后面处理接口返回的数据、拼接路径、收集异常记录,全是这个套路。等你把它写熟了,再换成推导式只是省几行字的问题,不是换思路的问题。

4.2 sum、min、max、len 的用法和空列表陷阱

Python 内置了四个统计函数,配合数字列表非常顺手。

函数作用空列表时的表现
sum(lst)求和返回 0,不报错
len(lst)元素个数返回 0
min(lst)最小值抛ValueError
max(lst)最大值抛ValueError

这个差异必须记牢:sum([])是 0,因为“没有东西加起来等于 0”符合数学直觉;但min([])会直接报ValueError: min() arg is an empty sequence,因为“空集合里的最小值是多少”这个问题本身没有答案。所以在算平均值的时候,如果分母是len(lst),空列表会导致除零错误ZeroDivisionError,得先判断。

scores = [88, 92, 79, 95, 61] if scores: # 空列表为假,直接跳过 avg = sum(scores) / len(scores) print(f"平均分:{avg:.2f}") print(f"最高分:{max(scores)},最低分:{min(scores)}") else: print("没有成绩数据")

if scores:这种写法比if len(scores) > 0:更地道,空列表、空字符串、空字典在条件判断里都是假值,用不着显式比长度。

4.3 列表推导式:把三行压成一行的正确姿势

推导式的基本形式是[表达式 for 变量 in 可迭代对象],后面还能挂if做过滤。

# 求平方 squares = [n ** 2 for n in range(1, 11)] # 只要偶数 evens = [n for n in range(1, 21) if n % 2 == 0] # 带条件的三元表达式 labels = ["及格" if n >= 60 else "不及格" for n in [58, 72, 91, 45]]

注意:推导式能塞两个 for 甚至三个,嵌套起来可读性会断崖式下跌。我的原则是——一眼看不明白的推导式,一律改回普通 for 循环。代码是给人维护的,省三行换来同事读十分钟,不划算。

另外一个容易忽略的点:推导式里的变量作用域是独立的,循环结束后不会污染外部同名变量。这跟普通 for 循环不一样,普通循环的变量会留下来。这个小差异在调试时偶尔会让人困惑,遇到了知道有这么回事就行。

5. 实操:用循环做几个能直接跑的小工具

5.1 成绩统计:从原始数据到一张汇总表

这是我在课上必做的第一个完整练习,因为它把循环、条件、统计函数全串起来了。

scores = [78, 92, 65, 88, 100, 54, 73, 89] total = 0 pass_count = 0 excellent = [] for s in scores: total += s if s >= 60: pass_count += 1 if s >= 90: excellent.append(s) avg = total / len(scores) print(f"人数:{len(scores)}") print(f"总分:{total}") print(f"平均分:{avg:.1f}") print(f"及格人数:{pass_count},及格率:{pass_count / len(scores):.1%}") print(f"90 分以上:{excellent}")

这段代码里其实藏了两个可以复用的模式。一个是累加器:先给total一个初始值 0,循环里不断往上加,最后得到总量。另一个是计数器:pass_count同样是先归零再加一。这两个模式几乎出现在所有实际项目里,认清它们比背语法重要得多。

顺带说一个常见错误:total的初始值写成total = 0是对的,但如果写total = 1,最后结果就多一;如果忘了初始化直接total += s,会报NameError: name 'total' is not defined。累加器的初始值一定是“加法的单位元”,也就是 0;要是做累乘,初始值就是 1。

5.2 嵌套循环:打印图形与乘法表

嵌套循环是第六课的难点,因为一旦两层套起来,执行次数的增长是指数级的:外层转 3 次、内层转 4 次,总共就是 12 次。新手在这里最容易搞混“谁在外谁在内”。

# 打印 5 层三角形 for i in range(1, 6): print("*" * i) # 九九乘法表(经典嵌套) for i in range(1, 10): for j in range(1, i + 1): print(f"{j}×{i}={i*j}", end="\t") print() # 内层结束换行

判断哪个循环在外层的技巧:看谁决定行,谁就放外面。乘法表是第 i 行有 i 列,行数由 i 控制,所以 i 在外层;内层的 j 从 1 走到 i,自然写成range(1, i + 1)。这个i + 1就是左闭右开的直接应用,因为要包含 i 本身,所以右边界得加一。

print()里的end="\t"是把默认的换行改成制表符,让同一行的几项横向排列;内层循环结束后单独调一次print(),相当于按一下回车。这两个细节不写,乘法表会变成一竖列,完全看不出结构。

5.3 用切片配合循环做批量处理

列表切片在第五课应该讲过了,但和循环结合时才真正显出威力。比较典型的需求是“每 3 条数据分一组”。

data = list(range(1, 21)) # 1 到 20 group_size = 3 for start in range(0, len(data), group_size): group = data[start:start + group_size] print(f"第 {start // group_size + 1} 组:{group}")

关键在于range(0, len(data), group_size)的步长用的是组大小,start依次是 0、3、6、9……切片的右边界写start + group_size,超出末尾时 Python 会自动截断,不报错。这个技巧在批量提交数据、分页展示时非常实用,比写一堆 if 判断边界省事多了。

不过要叮嘱一句:切片越界是安全的,但索引越界不安全。data[100]会直接抛IndexError: list index out of range,而data[100:200]只是返回空列表。分页那种场景务必用切片,别用索引硬取。

6. 常见问题与排查速查表

6.1 循环里改列表:最隐蔽的一类 bug

有一类问题我是反复强调的:不要在遍历列表的同时增删元素。

nums = [1, 2, 2, 3, 2, 4] for n in nums: if n == 2: nums.remove(n) print(nums) # 结果 [1, 2, 3, 4],还剩一个 2

明明想删干净,结果剩了一个,而且不报错。原因是 for 循环靠内部索引推进,删掉一个元素后后面的元素整体前移,索引却继续向后走,于是刚好跳过了一个。这个现象叫“边遍历边修改”,几乎是每个人都会踩一次的坑。

正确做法有两个:一是遍历副本for n in nums[:],改的是原列表、动的是副本;二是用推导式重建nums = [n for n in nums if n != 2]。我推荐第二种,语义清晰,也不容易想错。

6.2 排查速查表

报错或现象可能原因处理方式
SyntaxError: expected ':'for 那行末尾漏冒号在in xxx后面补冒号
IndentationError: expected an indented block冒号后没缩进循环体统一缩进 4 空格
TabError: inconsistent use of tabs and spacesTab 与空格混用全选后统一转空格,或开启空白字符显示
循环一次没执行range()边界反了或步长方向错用list()打印序列确认
结果少一个数range()右边界没加一记住左闭右开,需要包含就 +1
NameError: name 'total' is not defined累加器没初始化循环前写total = 0
ZeroDivisionError空列表求平均求平均前用if lst:判断
死循环循环体内手动改了i,或误用 while 忘自增检查是否在 for 里手动改循环变量
输出全是最后一项print被写到了循环外面检查缩进层级

6.3 几处容易忽略的小习惯

第一,循环变量别手动改。for i in range(10):里面写i = 5虽然合法,但下一轮开始 i 又会被range重新赋值,你的修改等于白写,只会让读代码的人困惑。

第二,range对象可以重复遍历。这点很多人不知道,以为自己得先转成列表。其实r = range(5)之后跑两次 for 都能正常拿到 0 到 4,因为它每次遍历都重新生成,不消耗自身。反而是生成器对象遍历一次就空了,那是后面的内容。

第三,需要下标的时候别用range(len(lst))硬取。能用enumerate()就用,虽然第六课可能还没讲,但知道有这么个东西,等学到时会恍然大悟。

第四,代码里出现魔法数字是大忌。写range(1, 100)的时候,最好中间变量先赋值student_count = 99,再写range(1, student_count + 1)。三个月后你自己看到range(1, 100)根本想不起来为什么是 100。

我个人在这几轮课里摸出来的几点体会

语言层面到这里已经能读懂大部分入门代码了,真正卡住人的从来不是语法,而是“我要用循环干什么”这个前置判断。每次布置练习,我都会强调先别急着打开编辑器,拿张纸把“输入是什么、循环转几圈、每圈做什么、最后输出什么”四件事写下来。写清楚了再敲代码,改bug的时间至少能砍一半。

还有一个我坚持了很多年的做法:每写完一个循环,一定先拿 3 到 5 条的小数据跑一遍,人工核对输出对不对,确认逻辑没问题了再换成完整数据。用真实的大数据集直接调试,出了错你都不知道是第几轮偏的,只能靠打满屏的 print 硬找。小数据先过一遍,这个习惯能帮你在后面学函数、学数据处理的时候省下大量时间。

最后补一句关于range边界的小技巧:我自己的习惯是每次写range,紧接着就在旁边写一行注释标出实际范围,比如range(1, 11) # 1 到 10。这条注释看着多余,但半年后回来改代码,你绝对会因为当初多写了这一行而感谢自己。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进