
循环结构基本上是Python基础里第一道真正意义上的分水岭。前面学变量、类型、分支判断的时候代码基本是从上往下顺着跑一趟哪里有问题一眼就能看到。到了循环这一步代码开始一段接一段反复执行思维必须从“顺序执行”切换到“迭代思考”很多同学就是在这个节点开始掉队的。实验五这7个关卡——斐波那契数列、统计英文语句中的单词数和句子数、密码判断、通过数列求自然常数e、双倍余额递减法计算固定资产折旧、销售统计、投资年限计算——题目看起来天南地北本质其实都在练同一件事循环的几种典型写法。for遍历、while条件循环、累加器、变量交换、循环边界控制一个都不少。把这7关老老实实吃透循环这个基本功基本就立住了。这篇我会按关卡逐个拆思路、给可运行的代码、讲容易踩的坑适合刚学完分支和循环、正卡在实验上的初学者也适合想把这组题当复习提纲的同学。1. 实验整体设计与思路拆解1.1 7个关卡其实是一条爬坡训练线先别急着动键盘把7道题放在一起看你会发现编排上很有讲究。题目类型和考点大概可以整理成这样一张表关卡题目核心考点主力循环第1关斐波那契数列递推迭代、变量交换for第2关统计单词数和句子数字符串遍历、split拆分for第3关密码判断布尔标记、多条件组合for / while第4关通过数列求自然常数e级数求和、累加器for / while第5关双倍余额递减法算折旧公式迭代、分阶段处理for第6关销售统计批量数据汇总、最值维护for / while第7关投资年限计算动态终止条件、复利迭代while前两关是在建立“重复做同一件事”的感觉斐波那契练递推字符串统计练遍历。第三关开始加入复杂的条件组合。第四、第五关把循环用在数学和财务公式上属于中等难度。最后两关直接上完整业务场景需要你根据题目描述自己设计循环结构。这个坡度是一点一点加大的。设计者的意图很明显循环不是背两个语法而是要养成一种“把重复过程抽象成迭代”的思维方式。我说句实在话见过太多同学在斐波那契这一关就把信心写没了原因不是题难而是他们上来就想“一次写对整个程序”。这个心态要不得。循环题的正确打开方式是先拆步再翻译最后再考虑边界。1.2 for循环和while循环到底怎么选很多初学者最纠结的就是这个题到底用for还是while我给出一个特别简单的判断标准能数清楚要循环多少次用for数不清楚、要靠某个条件来喊停用while。举个例子斐波那契要输出前n项n是输入进来的具体数字循环次数明确用for。投资年限计算里你根本不知道第几年能翻倍只能靠“账户金额小于目标金额”这个条件一直往下滚用while。销售统计如果题面说“统计N天销售额”N是已知的用for如果题面说“连续输入销售额直到输入0结束”那循环次数无法预知用while加break。这里多提醒一句for和while绝大多数情况下可以互相改写考试和实验里没有“必须用某一种”的死规定。但风格上要往“读起来顺畅”上靠。while循环最常见的问题就是忘了在循环体里更新条件变量导致死循环for循环则要小心range的区间range(1,n)和range(1,n1)差一个数结果天差地别。2. 前三关基础循环与字符串处理的正确姿势2.1 斐波那契数列两个变量滚出整个数列斐波那契数列的定义大家应该都熟第一项和第二项是1从第三项开始每一项等于前两项之和。这一关的常规要求是输入n输出前n项。最经典的写法是用两个变量滚动递推n int(input(请输入项数n)) a, b 1, 1 for i in range(1, n 1): print(a, end ) a, b b, a b print()拆开讲一下a存的是当前项b存的是下一项。每次循环先输出a然后立刻更新。a, b b, a b这行是Python特有的多变量同时赋值等价于做了两件事把原来的b给到a把原来的a加b给到b。要注意的是右边的ab用的是旧值a而不是新值a。这就是为什么不能用两条语句分开写# 错误示范 a b # a已经被覆盖成旧b了 b a b # 这里的a已经不是原来的a结果是错的如果你必须用分开写法需要引入临时变量temp a a b b temp b多看几遍这个交换逻辑它是后面很多算法的地基。这一关还要注意边界。n等于1时应该只输出一个1上面的代码用range(1, 2)只循环一次输出第一个a完全没问题。有些教材从0开始定义斐波那契0, 1, 1, 2, 3...那就把初始值改成a, b 0, 1具体以你实验题面为准。另外我知道有人会想到用递归def fib(n): return 1 if n 2 else fib(n-1) fib(n-2)递归写法思路清晰但这一关是练循环的别给自己加戏。而且递归做斐波那契有大量重复计算n一大就卡到怀疑人生。这个实验阶段老老实实用循环。2.2 统计英文语句中的单词数和句子数split比你想的聪明这一关说难不难说容易也有不少小坑。核心思路分两半单词数用split拆分字符串来数句子数通过统计句子结束符的个数来算。text input(请输入一段英文语句).strip() words text.split() sentence_count 0 for ch in text: if ch in .!?: sentence_count 1 print(单词数, len(words)) print(句子数, sentence_count)这里最值得展开的是split()的用法。直接写text.split()不传参数Python会自动按空白符切分而且会把连续的空格、Tab当成一个分隔符处理。比如输入I love Python用split()切出来是三个词I、love、Python。很多人踩过的坑是写成text.split( )按单个空格切。如果原文里有连续两个空格就会切出一个空字符串单词数莫名其妙就多了一个。所以除非题面明确要求按单个空格切否则务必用默认split()。句子数的规则要看清题目要求。英文句子的结束标志常见的是句号、问号、感叹号也就是.?三个字符代码里用in .?!判断每个字符是否属于这些标点。注意这里用in判断的是“字符是否存在这个字符串里”对单个字符来说没问题。如果题面上还要求统计中文的“。”或者省略号得按题面来别自己发挥。还有一个边界问题如果输入全是空格text.strip()一处理就变成空字符串split()结果是[],len为0不会崩。如果把strip()省了某些在线评测平台会因为你多输出一个空格判定答案错误。这里再分享一个思路判断句子数不一定非要用for逐个字符扫用text.count(.) text.count(!) text.count(?)也能得到结果。但实验的考点是循环用for遍历更能体现你在练什么。实际工作中我会用count但实验中我建议按老师要求的写法来。2.3 密码判断布尔标记法是这类题的万能解密码判断这一关在不同实验平台上的题面略有差异。有的版本是“输入一个用户名和密码判断是不是预设的admin/123456”有的版本是“判断密码的强度是否达到要求”。这里我按最常见、也最能练循环的“密码合法性校验”来拆解。题目逻辑通常是要求密码不少于8位且同时包含大写字母、小写字母和数字。这种题最清晰的解法就是用几个布尔变量做标记循环里逐个字符检查循环结束后统一判断pwd input(请输入密码) has_upper False has_lower False has_digit False has_special False for ch in pwd: if ch.isupper(): has_upper True elif ch.islower(): has_lower True elif ch.isdigit(): has_digit True else: has_special True if len(pwd) 8 and has_upper and has_lower and has_digit: print(密码合法) else: print(密码不合法至少8位且包含大小写字母和数字)这段代码的关键在于循环内部只负责“标记”不负责“下结论”。很多新手喜欢在循环里面边判断边输出结果一个密码里有多个大写字母就输出好几遍“检测到大写”整个逻辑乱成一团。把标记和判断拆开是这类题目从60分到90分的分水岭。这里再补一个重点有些人会用all()或any()写成一行比如any(ch.isupper() for ch in pwd)写起来很帅也确实Pythonic。但实验课上老师多半希望看到基础循环的写法我建议先把for循环版本写好跑通再自己私下研究一行写法两不误。如果题面上要求的是“不断输入直到密码合法才退出”那就是while True加break的典型用法while True: pwd input(请输入密码) has_upper any(ch.isupper() for ch in pwd) has_lower any(ch.islower() for ch in pwd) has_digit any(ch.isdigit() for ch in pwd) if len(pwd) 8 and has_upper and has_lower and has_digit: print(密码设置成功) break print(密码强度不足请重新输入)注意break的位置。break一旦执行整个while循环立即结束后面的print不会执行。如果把break放在print后面就会出现密码合法但是还提示重新输入的矛盾输出。3. 中间两关级数求和与业务公式的循环表达3.1 通过数列求自然常数e别去重算阶乘自然常数e的定义有很多种实验里通常用这个级数展开e 1 1/1! 1/2! 1/3! ... 也就是从第0项开始每一项是1除以n的阶乘一直累加。最直观但不太聪明的写法是每轮都重新算一次阶乘import math e 0.0 n int(input()) for i in range(n 1): e 1 / math.factorial(i) print(e)这样能跑但效率很一般而且有点“用循环作弊”的意思——阶乘本身就该用循环来算。更漂亮的做法是让每一项在前一项的基础上生成避免重复计算n int(input(请输入迭代项数)) e 1.0 item 1.0 for i in range(1, n 1): item / i e item print(f累加前{n1}项的结果{e:.10f})为什么这样对因为第i项是1/i!第i-1项是1/(i-1)!两者一除正好是1/i。也就是说只要把上一轮的item除以当前的i就能得到这一轮的新item。第一次循环时item还是1也就是第0项的1/0!除以1之后变成1/1!累加进e第二轮item再除以2变成1/2!以此类推。整个循环没有一次重复计算阶乘这也是“累加器”思想的经典体现。这一关还有个常见变体不告诉你迭代多少项而是要求“累加直到某项小于1e-10为止”。这种就用whilee 1.0 item 1.0 i 1 while item 1e-10: item / i e item i 1 print(fe≈{e:.10f})这里必须注意item初始值是第0项进循环后先除再累加。如果把顺序写成“先累加再除”第一轮就会把第0项重复加一次e直接偏大一个1。这种顺序问题非常隐蔽建议跑完和math.e对比一下养成交叉验证的习惯。浮点数精度也是一个考点。用float算级数求和算到十几项之后结果就很接近真实e了后面再加也不会提升太多。实验通常会要求输出保留几位小数用print(f{e:.10f})这样的格式化输出是最稳的。不要直接print(e)否则一长串浮点数可能被判定格式错误。3.2 双倍余额递减法最后两年要转直线法这一关是纯正的会计业务题第一次接触的同学容易懵。先花一分钟把概念捋清楚双倍余额递减法是加速折旧法的一种意思是固定资产在前期多提折旧、后期少提折旧。年折旧率是直线法的两倍也就是2除以预计使用年限但是每年的折旧基数不是原值而是年初的账面净值所以折旧额会逐年递减。如果不做特殊处理折旧额按固定比例递减账面净值永远减不到预计净残值所以会计准则规定在固定资产使用年限的最后两年要改用直线法把剩余账面净值减去净残值后平均摊销。这个“最后两年转直线法”是本题最大的考点。直接看代码cost float(input(固定资产原值)) salvage float(input(预计净残值)) life int(input(预计使用年限)) book cost # 年初账面净值初始等于原值 rate 2 / life # 双倍直线折旧率 for year in range(1, life 1): if life - year 1: dep (book - salvage) / (life - year 1) else: dep book * rate book - dep print(f第{year}年折旧额 {dep:10.2f}年末账面净值 {book:10.2f})逐行解释一下。book是滚动变化的账面净值每一年算完折旧都要减去折旧额因为它会作为下一年的折旧基数。rate 2 / life比如使用年限5年双倍折旧率就是2/50.4。判断条件life - year 1要仔细看。还是以5年为例第1年到第3年life - year分别是4、3、2都大于1走双倍余额递减第4年life - year等于1第5年等于0这两个都满足条件进入直线法分支。直线法的分母life - year 1第4年是2第5年是1相当于把“账面净值减去净残值”按剩余年限平均摊销。我拿一组数据验证一下原值100000净残值5000使用年限5年输出应该是这样年份折旧额年末账面净值第1年40000.0060000.00第2年24000.0036000.00第3年14400.0021600.00第4年8300.0013300.00第5年8300.005000.00注意第3年到第4年折旧额从14400突然降到8300这个跳变是正常的它就是“转直线法”的标志。最后一年账面净值刚好等于净残值5000完美闭环。这一关常见的错误有三个一是把rate写成2 // lifePython里的//是整除2//5结果是0折旧额全变0二是把折旧基数写成原值cost导致每年折旧额都一样那不是双倍余额递减那是直线法三是最后一年算完折旧后直接改book结果和题目期望的残值对不上检查一下是不是小数点精度问题建议用round或者格式化输出再比较。4. 最后两关业务场景下的循环实战4.1 销售统计最值初始化是个技术活销售统计这关不同平台题面多少有点差别常见的需求是输入若干天的销售额输出销售总额、平均销售额、最高销售额和最低销售额。核心代码不长但最值的初始化是新手翻车重灾区。先看固定天数版本的写法n int(input(请输入统计天数)) total 0.0 max_sale None min_sale None for i in range(n): sale float(input(f第{i 1}天的销售额)) total sale if max_sale is None or sale max_sale: max_sale sale if min_sale is None or sale min_sale: min_sale sale print(f销售总额{total:.2f}) print(f平均销售额{total / n:.2f}) print(f最高销售额{max_sale:.2f}) print(f最低销售额{min_sale:.2f})这里把max_sale和min_sale初始化成None而不是0是有讲究的。如果初始化成0那么当所有销售额都是负数时最高销售额永远算不对因为0比任何负数都大。虽然在销售场景里负数不常见但作为通用写法None加is None判断是更稳的。另一种常见写法是初始化为float(-inf)和float(inf)效果一样但None的方式对新手更好理解第一笔销售额进来不管是什么值先让它自己当上最大和最小。接着说平均值的除零问题。如果n输入的是0total / n会直接抛ZeroDivisionError。虽然正常评测不会给你n0但自己测试的时候发现崩溃不要慌知道是这个原因就行。如果题面改成“连续输入销售额直到输入0结束”那就要用while加哨兵值写法是这样的total 0.0 count 0 max_sale None min_sale None while True: sale float(input(请输入销售额输入0结束)) if sale 0: break total sale count 1 if max_sale is None or sale max_sale: max_sale sale if min_sale is None or sale min_sale: min_sale sale if count 0: print(没有输入任何销售额) else: print(f总销售额{total:.2f}) print(f平均销售额{total / count:.2f}) print(f最高销售额{max_sale:.2f}) print(f最低销售额{min_sale:.2f})这种while True加break的模式本质是把“读数据、判断结束、处理数据”这三件事按顺序放在循环体里。网上有些人喜欢搞“先读一次循环里末尾再读一次”的写法那个叫priming read也能用但新手很容易在末尾读那一步漏掉判断导致死循环。相比之下while True加break更不容易出错我平时写代码更倾向这种。4.2 投资年限计算while循环的最佳教学案例投资年限计算这关我觉得是7关里最能把while讲明白的一道题。场景很清晰你有初始本金年化收益率固定每年按复利滚一次问多少年后账户金额能达到目标金额。循环次数根本无法预先确定只能一个条件“while money target”一直跑这不就是while的天然主场吗。principal float(input(请输入初始本金)) rate float(input(请输入年化收益率如0.05表示5%)) target float(input(请输入目标金额)) money principal years 0 while money target: money * (1 rate) years 1 print(f需要 {years} 年)这个逻辑朴素得好理解只要钱没到目标就再滚一年。每滚一年years加1。这里money的更新用的是money * (1 rate)这是复利的表达方式利息计入下一年本金。为什么要用while而不是for因为这题的循环次数是个未知数。虽然你用对数公式n math.log(target / principal) / math.log(1 rate)能算出来一个人家是小数年比如14.2年但实际生活中我们问的是“第几年年底才够”答案只能是最小整数年。while迭代恰好能给出这个整数答案因为它是逐年模拟第15年跑完发现够了就输出15。这一关有两个非常经典的坑。坑一rate输入成5而不是0.05。利率是百分数还是小数题面一般会说明但很多人盯着代码看半天没发现问题最后才发现是输入格式的事。坑二rate等于0甚至负数时while条件永远为真程序直接死循环。参加在线评测时如果遇到“运行超时”先检查是不是这个原因。实际工程里我会加个保险while money target and years 1000: money * (1 rate) years 1这个1000是人为设置的最大年限防止极端参数导致死循环。实验里如果题面没要求加不加都行但加上了绝对不算错反而能体现你的工程意识。另外这题还有变体有的题面是“每年定额追加投资”那代码就变成每年先追加一笔钱再按利率滚一次本质还是这个while结构只是money的更新公式改了。做题时一定要先读清楚复利滚动的时机是按年结算、按月结算、还是先追加再结算这直接决定公式长什么样。5. 常见问题与排查技巧实录5.1 高频报错与排查速查表把这一组实验里遇到的高频问题整理成一张表遇到类似症状直接对号入座症状常见原因处理方式斐波那契输出乱套a, b更新顺序写错先覆盖了a再算b用a, b b, ab一次性完成单词数莫名偏多用split( )按单空格切多空格产生空串改成默认split()密码怎么输都判不合法某个has_xxx布尔变量忘了初始化循环外先全部赋Falsee算出来偏大或偏小累加顺序写反或者item更新位置错误确认先除后加第一项只加一次折旧最后一年对不上残值最后两年没转直线法确认life - year 1分支程序一直跑不结束while条件恒为真比如利率输入为0打印money观察变化或加最大循环次数平均销售额除零销售额数据一条都没进来加count 0判断类型转换报错直接把input()返回值当数字算用float()或int()包裹这个表是我自己在带新人时总结的基本覆盖了这7关里八成以上的报错现场。遇到问题先别急着乱改代码对着表找症状再往上翻对应的关卡看思路比来回试快得多。5.2 调试循环代码的三个土办法再说几个可以一路用到工作里的调试技巧。第一招叫print大法。在循环体末尾临时打印几个关键变量的值比如斐波那契里打印a和b投资年限里打印money和years跑一次就能看到每一步的变化过程。看到问题后记得把这些调试print删掉再交作业别问我怎么知道的。第二招叫缩小规模。把n改成3、5这样的小数字人工心算一遍结果再和程序输出对比。很多边界问题是在小规模下暴露的比如斐波那契n等于1、折旧年限等于2n一大你反而看不出规律。第三招叫先手推三步。拿纸笔把循环前三次迭代的变量变化写出来再对照代码看。这个习惯特别治“死循环”和“差一错误”。我带过的学生里只要肯手推三步循环正确率能提高一大截。如果这几个都试过还是不对再把代码里的人名、变量名和题面对一遍。很多时候不是逻辑错是题目看漏了比如“统计单词数”要求的是去重后的单词数或者“密码判断”要求的是判断预设密码而不是密码强度。题面歧义这种东西在线实验里经常出现答题前多花30秒精读题面比调试半小时划算。5.3 做完了这7关接下来可以玩什么如果你把这7关都跑通了循环基础实际上已经相当扎实。这时候别停去找几个能把循环用出新花样的练习巩固一下。常见的下一梯队练习包括用循环画等腰三角形、菱形用for循环实现冒泡排序或者用二分法猜数字。很多实验平台在循环这一章之后会紧跟一个“凯撒密码加密”的练习套路是while True包住输入内部用for遍历字符串把每个字符按偏移量移动后重新拼接。你写那个题的时候会发现写法上跟第三关密码判断高度相似既然能把密码判断做对凯撒密码对你来说也就是临门一脚的事。最后说一点我自己的体会。我带过的学生里能把循环学明白的几乎都有一个共同点不是代码背得多而是肯拿草稿纸把循环的前几次迭代手动推一遍。斐波那契的a、b交换折旧里的book滚动值投资里的money变化推三步循环的逻辑就长在脑子里了。这套实验做完后面学列表、字典、函数的时候你会发现自己比别人轻松一大截因为循环思维是所有数据结构的地基。地基打牢了楼随便盖。