循环结构基本上是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,n+1)差一个数,结果天差地别。
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。要注意的是,右边的a+b用的是旧值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"累加前{n+1}项的结果:{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为止”。这种就用while:
e = 1.0 item = 1.0 i = 1 while item >= 1e-10: item /= i e += item i += 1 print(f"e≈{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/5=0.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.00 | 60000.00 |
| 第2年 | 24000.00 | 36000.00 |
| 第3年 | 14400.00 | 21600.00 |
| 第4年 | 8300.00 | 13300.00 |
| 第5年 | 8300.00 | 5000.00 |
注意第3年到第4年折旧额从14400突然降到8300,这个跳变是正常的,它就是“转直线法”的标志。最后一年账面净值刚好等于净残值5000,完美闭环。
这一关常见的错误有三个:一是把rate写成2 // life,Python里的//是整除,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输入的是0,total / n会直接抛ZeroDivisionError。虽然正常评测不会给你n=0,但自己测试的时候发现崩溃,不要慌,知道是这个原因就行。
如果题面改成“连续输入销售额直到输入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, a+b一次性完成 |
| 单词数莫名偏多 | 用split(" ")按单空格切,多空格产生空串 | 改成默认split() |
| 密码怎么输都判不合法 | 某个has_xxx布尔变量忘了初始化 | 循环外先全部赋False |
| e算出来偏大或偏小 | 累加顺序写反,或者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、折旧年限等于2,n一大你反而看不出规律。
第三招叫先手推三步。拿纸笔把循环前三次迭代的变量变化写出来,再对照代码看。这个习惯特别治“死循环”和“差一错误”。我带过的学生里,只要肯手推三步,循环正确率能提高一大截。
如果这几个都试过还是不对,再把代码里的人名、变量名和题面对一遍。很多时候不是逻辑错,是题目看漏了,比如“统计单词数”要求的是去重后的单词数,或者“密码判断”要求的是判断预设密码而不是密码强度。题面歧义这种东西,在线实验里经常出现,答题前多花30秒精读题面,比调试半小时划算。
5.3 做完了这7关,接下来可以玩什么
如果你把这7关都跑通了,循环基础实际上已经相当扎实。这时候别停,去找几个能把循环用出新花样的练习巩固一下。常见的下一梯队练习包括:用循环画等腰三角形、菱形,用for循环实现冒泡排序,或者用二分法猜数字。很多实验平台在循环这一章之后会紧跟一个“凯撒密码加密”的练习,套路是while True包住输入,内部用for遍历字符串,把每个字符按偏移量移动后重新拼接。你写那个题的时候会发现,写法上跟第三关密码判断高度相似,既然能把密码判断做对,凯撒密码对你来说也就是临门一脚的事。
最后说一点我自己的体会。我带过的学生里,能把循环学明白的,几乎都有一个共同点,不是代码背得多,而是肯拿草稿纸把循环的前几次迭代手动推一遍。斐波那契的a、b交换,折旧里的book滚动值,投资里的money变化,推三步,循环的逻辑就长在脑子里了。这套实验做完,后面学列表、字典、函数的时候,你会发现自己比别人轻松一大截,因为循环思维是所有数据结构的地基。地基打牢了,楼随便盖。