for循环从入门到实战:语法、应用与避坑指南

发布时间:2026/10/4 3:26:15
for循环从入门到实战:语法、应用与避坑指南 很多刚接触编程的朋友都经历过这样的阶段想让程序输出1到100第一反应是把print语句复制粘贴一百遍后来学会了函数写了个print_1_to_100()总算不用在调用时复制粘贴了但每次改动范围还是得改函数体里的代码。直到某一天你第一次写下for i in range(100)这行代码才意识到所有重复的、有规律的劳动都可以交给循环去完成。for循环是所有编程语言中最基础、最常用的语法结构之一不管是Python、C、JavaScript还是Shell脚本它的身影无处不在。这篇文章我不打算照搬文档讲一堆抽象定义而是从一个实际写过大量循环代码的从业者视角把for循环的底层逻辑、基本语法、经典应用场景和踩坑经验掰开揉碎讲清楚。适合刚学编程不久、对for循环似懂非懂的同学也适合那些能跑通代码但说不清原理、遇到边界问题就发怵的初学者。1. 为什么几乎所有语言都离不开for循环1.1 没有循环时你只能手动展开重复代码想象一个场景老板让你写一个程序依次输出第1号员工到第10号员工。没有循环的情况下你得写十行几乎一样的代码print(第1号员工) print(第2号员工) print(第3号员工) # ... 一直写到第10行这还只是10条如果是1000条呢代码会膨胀到无法维护而且中间一旦想改格式比如把员工改成职员就得手动改十处甚至上千处极易漏改。循环的本质其实是把重复中有规律的操作抽象成一条规则告诉计算机从1开始每次加1一直到10为止每取到一个数就执行一次输出。这种抽象能力正是人类写代码区别于简单复制粘贴的核心价值。1.2 for循环背后的三步走逻辑无论你用的是哪种语言传统意义上的for循环都围绕三个核心动作展开初始化、条件判断、迭代更新。以C为例for (int i 0; i 10; i) { cout i endl; }这段代码的执行顺序是这样的初始化int i 0定义循环变量并赋初值这一步只执行一次条件判断检查i 10是否为真为真才进入循环体执行循环体输出i迭代更新执行i让i加1回到第2步继续判断条件直到条件为假时退出。这就是for循环的地基。理解了这三步你就理解了i为什么从0开始、为什么到9就结束、为什么退出循环后i的值停在10。而Python的for循环看起来很不一样它写成了for i in range(10): print(i)表面上是遍历一个叫range的对象但底层其实仍然包含初始化、判断、迭代这三个动作只是range对象帮你把这三步封装好了。Python的设计哲学是让常见的遍历操作更简洁所以你在语法层面看不到i这类更新操作但它做的事情和C是一样的。1.3 不同语言中的for循环形态对比很多初学者会在Python、C、JavaScript之间切换时感到混乱其实只要抓住初始化、条件判断、迭代更新这条主线切换成本就很低。下面这张表是我在实际项目中常用的对照语言基本写法特点Cfor (int i 0; i n; i)三段式最标准控制力最强Javafor (int i 0; i n; i)和C几乎一致另外支持增强forJavaScriptfor (let i 0; i n; i)注意用let而非var避免作用域问题Pythonfor i in range(n)遍历式写法简洁但控制力较弱Shellfor i in $(seq 1 10); do ...; done每次取一个值类似Python从这张表能看出一条规律C系语言的for循环倾向于给你三个自由度让你自己控制步长和条件而Python和Shell则倾向于把连续整数的生成交给工具你只管取用。两者没有绝对优劣只是适用场景不同。2. for循环基本语法逐个拆解2.1 循环变量与可迭代对象的关系Python里写for循环时最核心的一句话是for 变量 in 可迭代对象: 循环体这里的可迭代对象可以是列表、元组、字符串、字典、集合也可以是range对象。它的执行逻辑很直白每次循环开始前从可迭代对象中取出下一个元素赋值给变量然后执行循环体取完了就结束。我用代码展示几种遍历方式# 遍历列表 fruits [苹果, 香蕉, 橘子] for fruit in fruits: print(fruit) # 遍历字符串 for ch in hello: print(ch) # 遍历字典默认取键 info {name: 张三, age: 18} for key in info: print(key)理解把元素逐个取出来赋值给变量这个机制非常重要因为很多诡异问题都出在循环变量在循环结束后仍然保留最后一个值这件事上。比如for i in range(3): pass print(i) # 输出2循环结束后i依然存在值停留在最后一次迭代的取值。这在某些写法里可能是隐患但也可能被故意利用比如你想知道最后一次满足条件的元素是什么循环结束时直接取变量即可。2.2 range()的三种形式与边界条件range是Python中配合for循环最常用的工具它有三种形式range(stop) # 从0到stop-1 range(start, stop) # 从start到stop-1 range(start, stop, step) # 从start开始每次加step直到stop-1对应到代码里list(range(5)) # [0, 1, 2, 3, 4] list(range(2, 5)) # [2, 3, 4] list(range(1, 10, 2)) # [1, 3, 5, 7, 9]这里最需要记住一条铁律range的结束位置是不包含的也就是常说的左闭右开。range(1, 100)取不到100range(100)取不到100那取到的最大值分别是99和99。这条规则让无数新手困惑过后面我会专门讲它带来的坑这里先建立一个明确的认知。另外step还可以是负数用来做倒序遍历for i in range(5, 0, -1): print(i) # 5 4 3 2 1实际写倒序时很多人第一反应是range(5, 1, -1)结果发现取不到1因为结束位置同样不包含。想输出5到1就得写成range(5, 0, -1)这个细节靠记忆容易忘建议直接打印几次验证。2.3 循环体缩进与执行顺序Python用缩进表示代码块的归属for循环也不例外。缩进在for循环后面、且比for语句多一个层级的代码都属于循环体for i in range(3): print(进入循环体, i) print(循环结束后的代码)上面这段代码里print(进入循环体, i)会被执行三次而print(循环结束后的代码)只执行一次。缩进一旦不对轻则语法报错重则逻辑错误——比如你本想让某行代码在循环内反复执行结果因为少缩进了一格它变成循环结束后才执行一次。关于缩进我还有一个在实战中反复用到的观察循环体里尽量不要写太多行。如果一个for循环内部超过20行你就要警惕了通常意味着这段逻辑可以拆成函数或者你用错了结构。循环体越短越容易看清边界也越容易排查问题。2.4 嵌套循环与内外层变量for循环里再套for循环就是嵌套循环。最常见的入门例子是九九乘法表for i in range(1, 10): for j in range(1, i 1): print(f{j} x {i} {i * j}, end\t) print()这里有两个关键点一是内外层关系。外层循环每执行一轮内层循环都会从头开始完整执行一遍。所以当i1时内层j从1到1当i2时内层j从1到2……整体打印出来就是一个左下三角的乘法表。二是变量命名。内外层循环变量尽量不要用同一个名字否则内层覆盖外层外层之后要用就乱了。我习惯外层用i、内层用j、第三层用k虽然朴素但在团队协作中可读性很高。嵌套循环的另外一个常见用途是按坐标遍历二维数组。在处理矩阵、图像像素、表格数据时双重for循环几乎是标准操作。下一章我会结合具体例子展开。3. for循环的经典应用场景3.1 累加求和与计数统计for循环最朴素的用途之一就是对一批数据做累加或统计。核心套路是先设一个累积变量然后在循环体里反复更新它total 0 for num in range(1, 101): total num print(total) # 5050这个例子虽然简单但它体现了累积变量的通用思想。同样的模式可以扩展到求平均数先累加再除以个数求最大值用一个变量记录当前最大值每次拿新元素和它比较统计个数满足条件的元素每出现一次计数器加1。我举个例子统计1到100以内能被7整除的数的个数并求它们的和。count 0 total 0 for num in range(1, 101): if num % 7 0: count 1 total num print(f个数: {count}, 总和: {total})这个模式在处理数据报表时极其常用。比如从一个日志文件的循环读取中统计特定错误出现了多少次从传感器数据序列里统计超阈值的次数。套路都是一样的循环 条件判断 累积变量。3.2 遍历列表、字典时的高效姿势遍历列表有三种常见姿势我建议按需选择第一种直接遍历元素适合只需要值、不需要位置的场景for name in users: print(name)第二种用索引遍历适合需要位置、且可能修改列表中某个位置的值的场景for i in range(len(users)): print(i, users[i])第三种用enumerate同时取索引和值这是我在实战中最推荐的方式它比range(len(...))更Pythonic也少写一次下标访问for index, name in enumerate(users): print(index, name)遍历字典时也有讲究。默认for key in dict取的是键如果你想操作值要用for value in dict.values()如果想同时拿键和值用for key, value in dict.items()。初学者最容易忘记.items()导致拿不到值。这些都是for循环语法的一部分用多了自然就熟了。3.3 双重循环处理行列布局问题前端开发中一个经典需求是用for循环生成一组div要求一行显示两个元素且最后两个元素不加某个伪类样式。这个问题正好适合用for循环配合索引判断来解决。假设我们有10个元素渲染成5行两列。核心逻辑是for i in range(10): # 判断是否为行首元素索引是偶数时 if i % 2 0: print(新起一行) # 判断是否为最后两个元素 if i len(items) - 2: print(最后两个元素不加伪类) else: print(正常元素)这里面其实用到了两个for循环的经典技巧一是用i % 2 0判断是否处于行首位置从而实现每两个元素分组的布局逻辑 二是用len(items) - 2定位最后两个元素的索引避免给特殊样式。如果不用for循环纯手工给每个div写类名或样式代码将极度冗余且难以维护。而有了循环和索引判断一行两列、最后两列特殊处理这些需求就变成了纯粹的算术问题。3.4 从for循环引申到循环链表遍历热搜词里有个循环单链表它和for循环的关系很有意思。常规的单链表遍历通常是这样cur head while cur is not None: print(cur.value) cur cur.next但循环单链表的尾节点next指针指向头节点整个链表形成一个环。如果你照搬上面的while循环就会永远走不到None程序陷入无限循环。这时候就需要借助计数法来模拟for循环的边界控制# 已知链表长度为 n cur head for i in range(n): print(cur.value) cur cur.next你看思路一下子清晰了既然链表是环形的没有自然的结束条件那我们就用for循环遍历恰好n次来人为制造边界。这正是for循环相比while循环的一个重要优势——当循环次数明确时for循环天然自带边界保护。这个思想在实战中很有用。比如处理环形队列的数据、轮询多个设备状态、循环调度任务时只要明确了总次数用for循环永远比while循环更稳因为它天生不会因为退出条件写得不对而变成死循环。4. 控制循环走向的语法糖break、continue、else4.1 break遇到目标立刻停break的作用是立即终止整个循环不管循环条件还成不成立。最常见的场景是在一堆数据里寻找某个目标numbers [12, 35, 48, 91, 33, 77] for num in numbers: if num 50: print(找到第一个大于50的数:, num) break这段代码从列表开头找起找到第一个大于50的数91后立即跳出循环后面的33、77不再处理。如果没有break循环会一直跑到最后白白浪费时间这在小数据量时无所谓在百万级数据里就是数量级的性能差异。这里要特别注意一个特性break只能跳出它所在的那一层循环。如果你在双重for循环的内层写break它不会结束外层循环。想跳出多重循环可以用标志位、把逻辑封装到函数里用return或者Python还支持for-else结构来区分正常结束和被break中断这两种情况。4.2 continue跳过本次迭代但继续循环continue和break的最大区别是break终止整个循环continue只是跳过当前这一次迭代然后继续下一次。看这段代码for num in range(1, 10): if num % 2 0: continue print(num) # 只输出奇数当num为偶数时continue让程序回到下一次迭代跳过后面的print。最终输出的是1、3、5、7、9。如果用break则只会输出1就结束整个循环了。两者对比起来看更直观关键字作用典型场景break终止整个循环找到目标就停止遍历continue终止本次迭代继续下一次过滤掉不满足条件的元素我在处理日志数据时常用continue过滤掉无效行。比如遍历日志文件的每一行遇到空行或注释行就continue否则进入正式解析逻辑。这样能有效避免后续代码被空数据干扰。4.3 for-elsePython里的独门利器for-else是Python一个容易被忽视的语法糖它的逻辑是如果for循环正常走完没有被break中断就执行else块如果被break了else块就不执行。最经典的应用是判断素数。我们来看普通写法num 17 is_prime True for i in range(2, num): if num % i 0: is_prime False break if is_prime: print(num, 是素数) else: print(num, 不是素数)换成for-else的写法num 17 for i in range(2, num): if num % i 0: print(num, 不是素数) break else: print(num, 是素数)这里for-else的语义非常贴切如果整个循环从头到尾都没有找到能整除num的数也就是说没有任何一次迭代触发break那就说明num是素数走else分支。这个写法避免了额外的标志位逻辑更紧凑也更不容易出错。我第一次用for-else是在写素数表的时候当时还觉得循环后面跟else很怪用多了才发现这个设计其实非常符合思维习惯——它表达的就是如果真的找遍了都没有那就……。4.4 列表推导式把for循环压缩进一行列表推导式不是for循环的替代品而是for循环的一种语法糖变体。基本形式是[表达式 for 变量 in 可迭代对象 if 条件]比如要把1到10的平方收集成一个列表squares [x * x for x in range(1, 11)]等同于squares [] for x in range(1, 11): squares.append(x * x)还有带条件的版本比如只收集奇数的平方odd_squares [x * x for x in range(1, 11) if x % 2 0]列表推导式的优势是代码短、执行速度通常比手动append更快劣势是一旦嵌套两层以上可读性会断崖式下降。我的原则是单层推导式放心用双层推导式尽量别写实在要写就加注释说明每一层在干什么。5. 我踩过的for循环的坑与排查思路5.1 边界差一问题左闭右开关于range(1, 100)取不到100的问题我这里专门展开讲一下。很多初学者问我明明写了range(1, 100)为什么循环里的最大值是99答案是range的结束位置是开区间不包含。这是Python的设计惯例和字符串切片str[0:5]取不到索引5是同一套规则。这个惯例带来的好处是range(n)正好取n个元素从0到n-1两个range的长度可以直接相减得到。比如range(2, 8)的长度就是8 - 2 6规则统一不会出现到底该加1还是减1的混乱。但如果你是从C转过来的很容易踩坑因为C的for写法for (i 0; i 100; i)明确写的是而Python的range不写法符号全靠记忆。我的排查经验是写循环之前先问自己一句我这个循环想要遍历哪些具体数字然后用list(range(...))先打印出来看一眼确认边界再写循环体。5.2 循环中修改正在遍历的列表越删越乱这是for循环里最经典的坑之一。假设你有一个列表想删除其中所有偶数nums [1, 2, 3, 4, 5, 6, 7, 8] for num in nums: if num % 2 0: nums.remove(num)你猜结果是什么Python实际输出的是[1, 3, 5, 7]看起来是对的但如果换个列表再试比如[1, 2, 4, 5, 6, 7, 8]结果就错了。原因是for循环在遍历列表时是按索引递增取值的每删掉一个元素后面的元素会往前挪一位导致下一个待检查的元素被跳过了。这类问题正确的解决姿势有几种方案一遍历副本for num in nums[:]: if num % 2 0: nums.remove(num)方案二用列表推导式重建列表nums [num for num in nums if num % 2 ! 0]方案三倒序删除for i in range(len(nums) - 1, -1, -1): if nums[i] % 2 0: del nums[i]我个人最推荐方案二因为重建列表的方式最清晰、最不容易出错而且性能通常也不差。如果你只是想把偶数过滤掉完全没必要在原始列表上折腾。5.3 循环体内的动态追加导致伪死循环for循环理论上不会像while那样因为条件永远为真而无限循环但它有一种更隐蔽的伪死循环在遍历一个列表的同时往这个列表末尾追加新元素。lst [1, 2, 3] for item in lst: print(item) lst.append(item 100) # 每次迭代都追加新元素这段代码会一直输出1、2、3、101、102、103、201……永远停不下来因为迭代器每次以为还有下一个结果你每次都给它制造了下一个。这和热搜里提到的循环体中检测不到OK就死循环是同一类问题的不同变体。解决方案是明确边界如果你确定要操作初始的那一批数据先拷贝一份再遍历如果确实需要动态扩展建议改用while循环并加入次数上限作为保险max_iter 10000 count 0 while len(lst) 100 and count max_iter: # 处理逻辑 lst.append(...) count 1我给自己的习惯是任何循环都先问清楚退出条件是什么。尤其是那些需要持续检测外部状态比如等待设备返回OK的场景必须加上超时或最大尝试次数否则一个偶发异常就能让程序僵死在那里。5.4 大循环中的性能细节for循环写多了自然会遇到性能问题。我总结几条实用经验第一循环体内不要重复计算不变的值。比如for i in range(len(lst))如果len(lst)在循环过程中不变可以直接赋值给变量但Python的len本身很快问题不大更关键的是避免在循环体内重复做复杂的查找操作。比如要判断一个元素是否在大集合里应该先把集合建好而不是每次循环都去遍历一次列表。第二尽量避免在循环体内部使用append之外的高开销操作。比如字符串拼接在Python里频繁使用拼接字符串会产生大量中间对象更好的做法是收集到列表后用.join()一次性合并。第三嵌套循环要警惕复杂度爆炸。两层循环是O(n^2)三层是O(n^3)。处理1000个数据时两层循环是100万次操作还能接受但这如果是一个数据量级为10万的任务两层循环就是百亿次程序基本就卡死了。遇到这种场景优先思考能不能用字典、集合、哈希来降低一层循环的复杂度。我把这类问题统称为循环的代价。for循环语法本身很简单难的是知道什么时候不该用什么写法以及如何在不牺牲可读性的前提下降低循环带来的性能损耗。6. 一个综合练习用for循环找出1~100以内的所有素数并求和6.1 需求拆解素数质数的定义是大于1的自然数只能被1和它自身整除。用for循环来解这个问题需要两层循环外层循环遍历2到100的每一个候选数字内层循环从2遍历到该数字的前一个数判断能否被整除如果内层循环中找到了能整除的数就说明不是素数直接break如果内层循环整个跑完都没找到能整除的数就是素数加入总和。这个题目的价值在于它几乎把前面讲到的所有for循环知识点都串起来了range边界、嵌套循环、break、for-else、累加变量。6.2 朴素写法与for-else写法对照先看最直白的写法total 0 for num in range(2, 101): is_prime True for i in range(2, num): if num % i 0: is_prime False break if is_prime: total num print(total)再用for-else简化total 0 for num in range(2, 101): for i in range(2, num): if num % i 0: break else: total num print(total)两种写法结果一样但for-else版本少了一个is_prime标志位逻辑上更干净。这里的else块表达的语义是内层循环没被break说明2到num-1之间没有任何数能整除num那num就是素数。运行结果应该是总和1060你可以自己跑一下验证。这个经典题目我面试过不少候选人能写出for-else版本的不多但一旦写出来基本能确认对方是真的理解for循环的控制流而不只是背语法。6.3 优化与扩展从暴力到高效上面这个写法在100以内很快但如果让你求1到100000以内的所有素数之和内层循环每次都从2跑到num-1总计算量会非常惊人。两个优化思路第一个优化内层循环只需要到sqrt(num)即可。因为如果一个数num有因数那么它必然有一个因数小于等于sqrt(num)。代码改成import math total 0 for num in range(2, 100001): for i in range(2, int(math.sqrt(num)) 1): if num % i 0: break else: total num print(total)第二个优化先排除偶数。大于2的偶数必然不是素数所以外层循环可以只遍历奇数起步再单独处理2。import math total 2 # 先把2算进去 for num in range(3, 100001, 2): for i in range(2, int(math.sqrt(num)) 1): if num % i 0: break else: total num print(total)这里range(3, 100001, 2)验证了前面讲的step参数步长为2正好取全部奇数。两个优化叠加计算量能减少一大截。再进一步如果你要做的是列出2到N的所有素数更专业的做法是埃拉托斯特尼筛法它同样基于循环思想但内层循环不再是逐个判断而是批量标记合数。这算是对for循环应用的一个进阶延展有兴趣的可以自己去查。最后再分享一点我在实际写代码中的体会for循环这类基础语法千万不要死记硬背写法而是先想清楚三件事——我要遍历什么我要对每个元素做什么循环结束后我期望得到什么状态把这三个问题用一句话写出来再转化成for循环的代码基本不会出错。另外写任何涉及边界的循环前先小范围验证一下range的取值范围看起来是个笨办法但比事后调试节省的时间多得多。