Python循环结构实验详解:从斐波那契到投资计算,7道题突破编程分水岭
1. 实验整体设计与思路拆解1.1 为什么循环结构是Python入门的分水岭我带了这么多届学生的Python实验课几乎每次讲到循环结构这一章班级里就会明显分出两个梯队一部分人从这里开始真正“会写程序了”另一部分人则在这里开始觉得“好像听不懂了”。原因很简单——顺序结构其实不需要你思考从上到下读代码就行分支结构也只是多了一条路的选择但循环结构是第一个强迫你用“机器思维”去思考的结构你得真正理解什么是重复、什么是终止条件、什么在变化。这个实验七道题从斐波那契数列到投资年限计算表面上看是把循环的多种应用场景走了一遍实际上它是在训练你三件事循环变量的设计、循环边界的控制、循环体的累积逻辑。这三件事看起来简单但每道题都在用不同的方式考你。比如斐波那契数列考的是“滚动更新”多个变量的能力单词句子统计考的是“状态切换”的判断思路密码判断考的是“标志位”的用法自然常数e考的是“累加器”的写法双倍余额递减法考的是“保留值迭代”的思想销售统计考的是二维累积的嵌套逻辑投资年限计算考的则是最经典的“循环变量与业务条件耦合”的场景。如果你能把这个实验从头到尾独立完成而且不是靠背代码是真的自己想明白每一行为什么这么写那你的循环结构就算是真正过关了。我下面把每一关的关键思路和我在实际批改作业时发现的高频问题都拆开讲一遍代码全部用Python 3来写。1.2 七道题之间的递进关系先说个整体印象这个实验的题目顺序是精心设计过的不是随意排列的。前三道题斐波那契、单词句子统计、密码判断属于“单层循环就能解决”的基础题它们主要考察你对循环控制流本身的掌握尤其是循环内部的条件分支怎么和循环变量配合。这三道题做对了说明你已经掌握了循环的基本操作。第四题自然常数e稍微升级了一点它要求你在循环里做累加而且是“未知次数”的累加——你需要根据精度条件来决定什么时候结束循环这就是while循环的真正用武之地。for循环通常是“我知道要循环多少次”while循环是“我不知道要循环多少次但我知道什么时候该停”这是一次重要的思维切换。第五题和第七题双倍余额递减法、投资年限计算是“逼近类”问题——你需要循环迭代直到某个值达到条件折旧后的账面价值降到预计净残值以下或者投资资产达到目标金额。这类问题在真实业务中极其常见。第六题销售统计则是把循环和列表、字符串处理结合起来需要你处理的是“一组数据”而不是单个变量思路又不一样。理解了这个递进关系你做题的时候就不会觉得每道题都是孤立的。我建议你做每一题之前先问自己三个问题这个循环要重复做什么事情重复多少次或者什么时候停下来每次循环有哪些变量在变化这三个问题想清楚了代码基本上就能写出来了。2. 第1关斐波那契数列——循环变量的滚动更新2.1 题目要求与核心思路斐波那契数列是算法入门的经典问题了数列的前两项通常是1和1有的版本是0和1从第三项开始每一项都等于前两项之和即 F(n) F(n-1) F(n-2)。题目一般会让你输入一个整数n输出前n项或者第n项。我第一次教这个题的时候发现很多同学的第一反应是去查“斐波那契数列的公式”试图用一个简单的数学表达式直接算出来。这其实是思维方向错了——这个题目放在循环结构实验里考察的就是你用循环去“递推”这个数列的能力不是让你套公式。而且斐波那契数列的通项公式里带有根号和幂运算在计算机里用浮点数计算反而容易产生精度误差远不如循环递推来得干净利落。递推的核心思路是这样的维护两个变量分别表示“当前项的前一项”和“当前项”每次循环让这两个变量都向前滚动一次。2.2 参考实现与逐行讲解n int(input(请输入需要打印的斐波那契数列项数)) a, b 1, 1 if n 1: print(a) else: for i in range(n): if i 0: print(a, end ) elif i 1: print(b, end ) else: c a b print(c, end ) a, b b, c这段代码的逻辑很直白a和b初始化为1和1代表数列的前两项。循环从第0项开始遍历前两项分别原样输出从第三项i等于2开始c等于a加b然后输出c同时把a更新为原来的b把b更新为新的c这样就保证了下一轮循环时a和b仍然分别是“前一项”和“当前项”。Python里a, b b, c这种写法是同时赋值右边的值先全部计算完再统一赋给左边的变量所以不需要临时变量。这个写法看起来只是语法糖但它背后体现的其实是“滚动更新”的思想——三个变量像传送带一样每次循环都往前挪一个位置。2.3 常见问题换行输出和负数的坑很多同学在输出斐波那契数列时喜欢一个数一行比如用普通的print(c)但题目如果要求“一行输出”或者“空格分隔”就需要在print函数里加上end 参数把默认的换行符换成空格。忘记加这个参数是这道题最高频的丢分点。另一个容易踩的坑是输入n为负数或者0的时候怎么办严格来说斐波那契数列只对正整数有意义。但如果你不主动处理for range(负数) 根本不会执行循环体程序直接结束用户看到的是“没有输出”的诡异现象。我在实验课上要求学生必须做输入合法性判断至少有这样一个保护if n 0: print(请输入正整数)2.4 扩展不只是输出数列还能求第n项如果题目改一下只让你输出第n项比如输入10输出55代码反而更简洁n int(input()) a, b 1, 1 for i in range(2, n): a, b b, a b print(b)注意这里range从2开始因为前两项已经由初始值占住了。当n等于1时循环体一次都不执行直接输出b也就是1这其实是正确的结果。这个小细节特别能体现你对边界条件的把握——很多同学在一开始给a和b赋值1、1之后总觉得还需要特别判断n等于1的情况但其实range的边界已经天然处理好了。这个“滚动更新”的思路在后续很多算法里都会用到比如字符串轮转、数组移位、甚至一些状态机的问题。学斐波那契不只是学一个数列更重要的是学会“用有限个变量去跟踪无限递推的过程”。3. 第2关统计英文语句中的单词数和句子数——状态切换与边界处理3.1 题目难点在于定义“单词”和“句子”这个题表面上看起来不难给你一段英文统计里面有多少个单词、多少句话。但真正动手写的时候很多同学会卡住因为“什么是单词”这个定义本身就值得商榷。举几个例子Hello,world!里面的逗号算不算分隔符“dont”里面的撇号算不算单词的一部分“ ”连续多个空格怎么处理题目一般会给定简化规则最常见的规定是单词由连续的英文字母组成单词之间以空格或其他非字母字符分隔句子以句号.、问号?或感叹号!结尾。如果你不做任何约定直接数空格个数来统计单词数遇到Hello, world这种句子时逗号后面有空格空格数等于1但单词数是2逻辑上没问题。可一旦遇到Hello,world——逗号后面没有空格——空格数是0但你显然应该把它算作两个单词。所以数空格这种方法本质上是有缺陷的。3.2 状态切换法用布尔变量记录“是否处在单词中”正确的做法不是数分隔符而是扫描每个字符判断字符类型用状态记录当前是否正处于一个单词内部。我给出一个比较稳的实现text input(请输入一段英文语句) word_count 0 sentence_count 0 in_word False for ch in text: if ch.isalpha(): if not in_word: word_count 1 in_word True else: in_word False if ch in .?!: sentence_count 1 if text and not text[-1].isalpha(): sentence_count sentence_count else: sentence_count sentence_count print(单词数, word_count) print(句子数, sentence_count)这段代码的核心逻辑在循环体里遇到字母字符ch.isalpha()为True说明当前处于某个单词之中。如果上一个字符不是字母in_word为False说明这个字母是一个新单词的开头单词数加1然后把in_word置为True。遇到非字母字符说明单词结束了把in_word置为False。如果这个非字母字符是句号、问号或感叹号句子数加1。这种“状态切换”的思路非常经典。in_word这个布尔变量相当于一个状态标志它记录的是“当前位置是否处在一个单词内部”。你不需要去数空格也不需要处理连续空格的问题——因为连续空格只会让in_word从True变False再变False但不会触发单词计数。3.3 边界情况末尾标点、空字符串和中文引号在实际测试中有几个边界情况一定要专门测试第一字符串末尾的标点。如果文本以句号结束那么最后一个字符是句号ch.isalpha()为False所以in_word在循环结束前去被置为False单词数不会多算句子数正常加1。这没问题。第二空字符串。直接输入空字符串循环体一次都不执行word_count和sentence_count都是0输出0和0逻辑上没有bug但如果你在输出之前想加上“没有任何内容”的提示需要手动判断。第三引号和撇号。英文单词中的撇号比如dont在Python里用isalpha()判断时撇号不是字母所以会被当作单词分隔符dont会被计为两个单词don和t。这在严格意义上是有争议的。如果题目要求忽略撇号你就需要在判断非字母字符时单独排除撇号的情况elif ch : continue第四字符串中的数字。isalpha()对数字返回False所以数字会被当作分隔符abc123def会被统计为两个单词。这在多数场景下是符合预期的。第五没有细节到“小数点和单词相连”的情况。比如OpenAIs GPT-4.这样的字符串会统计出OpenAIs会被当成一个单词、GPT、4数字作为分隔符等结果可能和人的直觉有出入。这时候就要看题目是否对符号做了更细的规定。我在实际批改中会给学生强调这个题没有必要追求“完全智能”只要严格按题目给定的规则实现测试点全过即可。3.4 用另一种思路正则表达式如果你已经学过了正则表达式这个题可以写得更短import re text input(请输入一段英文语句) words re.findall(r[A-Za-z], text) sentences re.findall(r[.!?], text) print(单词数, len(words)) print(句子数, len(sentences))re.findall会把所有匹配的子串收集成一个列表len()数个数即可。[A-Za-z]表示“连续的英文字母”[.!?]匹配任意一个句子结束标点符号。这种写法更接近“按规则切片”的思路代码简洁很多不过它依赖正则表达式的基础如果老师还没讲到用之前要先了解一下它的语法不然看不懂自己写的代码就尴尬了。4. 第3关密码判断——标志位与多重条件4.1 题目要求拆解第3关通常是这样用户输入一个密码字符串程序判断它是否符合强度要求。最常见的规则是长度不少于8个字符必须同时包含大写字母、小写字母和数字有些版本还会要求包含特殊字符。符合条件输出“密码合格”否则输出“密码不合格”。这道题放在循环结构这一章里其实有点“四两拨千斤”的意思。它虽然考察的是循环遍历字符串中每个字符然后分类判断但真正考查的核心是如何同时维护多个独立条件的满足状态。很多同学会犯一个很典型的错误——试图用一个if-elif-else链来处理所有条件结果写出来要么逻辑混乱要么判断不出同时满足多类字符的情况。4.2 推荐实现四个布尔标志位password input(请输入密码) has_upper False has_lower False has_digit False length_ok len(password) 8 for ch in password: if ch.isupper(): has_upper True elif ch.islower(): has_lower True elif ch.isdigit(): has_digit True if has_upper and has_lower and has_digit and length_ok: print(密码合格) else: print(密码不合格)核心就是四个标志位has_upper、has_lower、has_digit分别记录是否出现过至少一个大写字母、小写字母和数字length_ok记录长度是否达标。遍历完整个字符串之后四个标志位做逻辑“与”运算全True才合格。4.3 不要用elif的陷阱我发现很多初学者会这样写for ch in password: if ch.isupper(): has_upper True if ch.islower(): has_lower True if ch.isdigit(): has_digit True这里用三个独立的if其实没问题。但有同学会写成elif也就是if ch.isupper(): has_upper True elif ch.islower(): has_lower True elif ch.isdigit(): has_digit True这个写法在逻辑上是错的因为一个字符不可能同时是大写字母、小写字母和数字用elif虽然对单个字符来说结果一样但如果遇到一个字符同时满足两个条件比如某些特殊字符在Unicode里既被归类为数字又……这种情况不常见但理论上存在elif链会漏掉后面的判断。更重要的是这种写法很容易让初学者形成错误的思维定势。用独立的if语句语义是“分别检查三种情况”用elif链语义是“三种情况只能命中一种”这不符合这里的业务逻辑——你在一个密码里需要同时存在三种字符而不是某个字符同时是三种类型。4.4 更Pythonic的写法any() 生成器如果你已经对Python的生成器和内置函数比较熟悉可以这样写password input(请输入密码) is_strong ( len(password) 8 and any(ch.isupper() for ch in password) and any(ch.islower() for ch in password) and any(ch.isdigit() for ch in password) ) print(密码合格 if is_strong else 密码不合格)any()接收一个可迭代对象只要其中有一个元素为真就返回True。这里any(ch.isupper() for ch in password)等价于“密码中至少存在一个大写字母”。这个写法的好处是代码更简洁少写了三个布尔变量坏处是初学者可能会觉得不好理解尤其any()搭配生成器的组合需要一定的Python功力。如果实验报告要求“用循环结构实现”建议还是老老实实写标志位版本并且在注释里说明思路。4.5 扩展给登录流程加密码判断这一关如果玩得深入一点可以把循环知识和后续的登录流程串联起来。很多同学在做完这一关后觉得自己已经会密码判断了但遇到“用户输入密码错误则重新输入最多尝试3次”这种需求时又不知道该怎么做了。这就是while循环的天然应用场景password abc123ABC for attempt in range(3): user_input input(请输入密码) if user_input correct_password: print(登录成功) break else: print(密码错误还剩, 2 - attempt, 次机会) else: print(多次尝试失败账户已锁定)把for循环和else搭配起来如果for循环自然结束也就是说从未break则执行else分支。这在“尝试N次”的场景下特别方便比用while加计数器更直观。5. 第4关通过数列求自然常数e——浮点数累积与精度控制5.1 数学原理与题目预期自然常数e的经典定义为e 1/0! 1/1! 1/2! 1/3! ...也就是说e等于从0开始的阶乘倒数的无穷级数之和。0!按数学定义等于11!等于12!等于23!等于64!等于24……所以前几项加起来1 1 0.5 0.1666... 0.0416... ≈ 2.7083...随着项数增加越来越接近e的真实值 2.718281828...这个题目在设计上其实没有特别大的难度但它有一个很关键的数学点每一项的分母是阶乘。如果你在循环里每一项都重新算一次阶乘效率就很低。正确做法是在前一项的基础上乘一个数得到下一项。5.2 参考实现迭代计算每一项的倒数import math precision 1e-10 e 1.0 term 1.0 n 0 while True: n 1 term / n e term if term precision: break print(计算得到的e值, e) print(math库中的e值, math.e) print(迭代次数, n)这个实现的精髓在term / nterm初始为1代表1/0!。第一次循环term变为1/11加到e上得到2.0第二次循环term变为1/20.5加到e上得到2.5第三次term变为0.5/30.1666...加到e上……每一次都在上一项的基础上除以一个递增的整数n就得到了1/n!。这样写不仅代码简洁而且计算速度快根本不需要每次重头算阶乘。5.3 循环结束条件的选择题目一般会问你“循环到什么时候停止”最常见的做法有两种第一种迭代固定次数。比如要求计算前100项的和。这时候直接用for循环e 1.0 term 1.0 for n in range(1, 101): term / n e term print(e)这种做法简单可靠但如果项数取得不够多累积误差会比较大。100项的时候term已经是1/100!这个值小到可以忽略不计所以用100次迭代基本能保证双精度浮点下的极限精度。第二种根据精度动态停止。像上面代码里的if term precision: break每次项小于设定阈值比如1e-10就停止。这种做法更“科学”因为float类型的精度大约在1e-16量级把阈值设成1e-10时得到的e已经和math.e在12位有效数字内完全一致了。5.4 浮点数的精度警告这里必须提醒一个容易踩的坑不要用 while e ! math.e 这种条件。浮点数在计算机里是近似存储的两个浮点数永远不会“完全相等”。你应该用“差的绝对值小于某个阈值”来判断if abs(e - math.e) 1e-8: print(精度已达要求)另外如果你的程序一开始就把e初始化为1.0而不是0.0记住是因为级数的第一项1/0!等于1这不难但很多同学容易在n的初始值上搞错。你可以把n从0开始也可以从1开始关键是保证term每次除以的n和它实际表示的分母阶数是对应的。我通常建议n从1开始第一步计算1/1!最不容易出错。5.5 实验报告的优化输出小数位数如果题目要求保留若干位小数用格式化输出print(e的值约为{:.10f}.format(e)){:.10f}表示输出浮点数的10位小数。这里注意{:.10f}做的是四舍五入不是截断。如果你需要截断而不是四舍五入可以用int(e * 10**10) / 10**10来实现不过大多数情况下四舍五入就够了。6. 第5关双倍余额递减法计算固定资产折旧——迭代逼近与边界判断6.1 业务背景这到底是什么算法双倍余额递减法Double Declining Balance Method是会计学里常用的一种固定资产加速折旧方法。它的核心思想是在固定资产使用寿命内每期折旧额等于该期期初账面净值乘以一个固定的折旧率这个折旧率是“直线法”折旧率的两倍。举个例子一台设备原值100000元预计使用5年预计净残值5000元。直线法的年折旧率是1/520%双倍余额递减法的年折旧率就是40%。第一年折旧额 100000 × 40% 40000元账面净值降到60000元第二年折旧额 60000 × 40% 24000元账面净值降到36000元以此类推。双倍余额递减法有个特殊的处理规则在固定资产使用的最后两年要用“账面净值减去预计净残值后的余额”在剩余年限内平均摊销。这是因为加速折旧在前几年折得很快如果不改成直线法会导致最后账面净值低于甚至低于净残值。具体要求是当某一年按双倍余额递减法计算的折旧额小于用“剩余账面净值-净残值后按剩余年限平均分摊”的金额时从该年起改用直线法计提折旧。简化版本的实验题目通常是每年计算折旧但当账面净值减少到小于或等于预计净残值时最后几年就改为平均摊销。6.2 核心公式与循环退出条件你不需要完全掌握会计学的所有细节但必须理解下面这几个核心变量固定资产原值cost预计净残值salvage预计使用年限life以年为单位年折旧率rate 2 / life每年折旧额 当前账面净值 × rate但要注意不能超过某个上限这个上限是如果某一年按双倍余额递减法算出的折旧额会比把剩余价值当前账面净值 - 净残值在剩余年限内平均分摊的数值还要小那么从这一年开始就改用平均分摊的方式来计提折旧。在实际的实验题里常见的简化规则是后两年改为平均摊销也就是说前life-2年用双倍余额递减法最后两年把剩下的账面净值减去残值后平均分摊。6.3 参考实现按完整年限循环cost float(input(请输入固定资产原值)) salvage float(input(请输入预计净残值)) life int(input(请输入预计使用年限)) rate 2 / life book_value cost total_depreciation 0 for year in range(1, life 1): if year life - 2: # 最后两年改为平均摊销 annual_depreciation (book_value - salvage) / (life - year 1) else: annual_depreciation book_value * rate # 防止过度折旧导致账面净值低于净残值 if annual_depreciation (book_value - salvage) / (life - year 1): annual_depreciation (book_value - salvage) / (life - year 1) book_value - annual_depreciation total_depreciation annual_depreciation print(f第{year}年折旧额{annual_depreciation:.2f}账面净值{book_value:.2f}) print(f累计折旧额{total_depreciation:.2f}) print(f最终账面净值{book_value:.2f}) print(f应保留的净残值{salvage:.2f})这段代码做了几件事第一行到第三行从用户输入读取原值、残值和年限然后计算年折旧率rate 2 / life。循环里做了年份判断前life-2年按双倍余额递减法计算折旧额但每次都要检查一个条件——如果今年按双倍余额递减法算出来的折旧额比“把剩余账面净值减残值后按剩余年限平均分摊”的金额还要大那就说明再按加速折旧会导致账面净值掉到净残值以下这时候就直接改用给定的剩余年限平均摊销。最后两年无论什么情况都改为平均摊销。6.4 为什么必须加“防止过度折旧”的判断这个细节是这个实验的精华所在。如果不加这个判断只按固定公式每年乘以rate你自己跑一遍就能发现第五年折旧额 36000 × 40% 14400元折旧后账面净值 21600元这已经大于净残值5000元看似没问题但继续用双倍余额递减法的话第六年折旧额 21600 × 40% 8640元折旧后账面净值 12960元第七年折旧额 12960 × 40% 5184元净值 7776元还要继续折旧……最终会把账面净值折成趋近于0或者低于净残值这就违反了固定资产折旧的基本原则账面净值不能低于预计净残值。所以这里必须有条件判断当某年按加速法计算的折旧额大于剩余价值平均分摊的金额时改用平均摊销。业务逻辑就是折旧可以快但不能快到最后亏本。6.5 常见错误混淆“账面原值”和“账面净值”我批改这类实验作业时经常看到学生把折旧基准搞错。折旧是按“账面净值”来算的不是按“账面原值”来算的。也就是说第一年按原值100000算折旧第二年就只按60000算折旧了第三年按36000算。很多同学写出来的代码每年都用固定的cost去乘rate算出来的折旧额一模一样这显然是错的——双倍余额递减法之所以叫“余额递减”就是因为它的计算基数是逐年递减的余额。另外输出格式要求保留两位小数时记得用{:.2f}格式化不格式化的话浮点数可能出现很长的一串小数尾巴影响阅读。7. 第6关销售统计——二维数据累积7.1 题目实际考察点第6关通常会给出一组销售记录比如“sales [{商品: A, 数量: 3, 单价: 10}, ...]”或者更简单地用列表嵌套让你统计总销售额、各商品销量排行等。这道题考察的是“把循环用于聚合计算”本质上是一个二维数据的累积问题。如果你已经学过了字典这道题会轻松很多如果还没有学到用列表也能做只是麻烦一点。7.2 一个简单版本统计总销售额假设销售数据是以列表形式给出的每笔交易每笔交易是一个元组商品名, 数量, 单价sales [ (苹果, 3, 5.0), (香蕉, 2, 3.5), (苹果, 1, 5.0), (橙子, 5, 4.0), (香蕉, 4, 3.5), ] total_sales 0 item_sales {} for item, quantity, price in sales: amount quantity * price total_sales amount if item in item_sales: item_sales[item] amount else: item_sales[item] amount print(总销售额, total_sales) for item, amount in item_sales.items(): print(f商品{item}的销售额{amount:.2f})这里的关键是充分使用Python的序列解包特性for item, quantity, price in sales直接遍历列表同时解包出每一个元素里的三个字段。这个写法的好处是你不需要用索引sales[i][0]去取字段代码可读性高很多而且不易写错。后面统计每个商品的销售额时用一个字典做“按key累积”的操作。这个模式在真实的数据处理非常常用遇到一个数据项先查字典里有没有这个key有就更新没有就新增。这个“先查再更新”的模式用官方语法有一个更简洁的替代方案from collections import defaultdict item_sales defaultdict(float) for item, quantity, price in sales: item_sales[item] quantity * pricedefaultdict会在访问不存在的key时自动创建一个默认值通常是0.0所以item_sales[item] amount第一次执行时会自动创建key为item、值为0.0的条目然后加上amount。这比手动判断key是否存在要干净得多。7.3 计算销售数量合计、折扣后金额等有些题目不只是让你算销售额还会加一些额外条件比如“数量大于等于10时打九折”。这时候直接在循环体里加一个条件判断即可for item, quantity, price in sales: amount quantity * price if quantity 10: amount * 0.9 total_sales amount如果题目还要求统计“每种商品的销量合计”“平均单价”等可以把每个商品的销售数量和销售金额分别用两个字典累积。注意这里有个容易混的点平均单价不等于简单平均而是总销售额除以总销量也就是加权平均。这个业务概念如果理解错了统计结果会差很多。7.4 让结果更清晰的格式化输出如果销售数据很大你可能想对结果排序展示。可以用sorted()函数按销售额降序排列sorted_items sorted(item_sales.items(), keylambda x: x[1], reverseTrue)item_sales.items()返回的是商品名, 销售额的键值对列表keylambda x: x[1]表示按第二个元素销售额排序reverseTrue表示降序。Lambda表达式是Python里比较有特色的语法很多刚开始学的同学会觉得陌生。你可以粗暴地把它理解成一个“临时的小函数”在这里就是这个临时函数接收一个键值对返回它的第二个元素作为排序依据。不想用lambda也可以直接写operator.itemgetter(1)达到相同效果但那需要先import operator模块反而多了个概念。8. 第7关投资年限计算——while循环与条件退出8.1 题目业务逻辑第7关是典型的财务计算问题你有一笔本金存入银行或投资某个年化收益率的项目每年复利结算一次问需要多少年才能达到目标金额。题目给出本金P、年化收益率r百分比或小数形式、目标金额T要求计算需要的年数。数学公式是期末金额 本金 × (1 年化收益率)^年数。你需要找到最小的整数年数n使得 P × (1r)^n ≥ T。这个问题用数学公式直接解当然也可以两边取对数但实验题目显然想让你用循环来模拟“逐年滚雪球”的过程。用循环模拟的好处是更直观而且不需要对数运算。8.2 参考实现principal float(input(请输入本金元)) annual_rate float(input(请输入年化收益率%例如5表示5%)) / 100 target float(input(请输入目标金额元)) current principal years 0 while current target: current * (1 annual_rate) years 1 print(需要, years, 年才能达到目标金额) print(届时金额为, current)这段代码的核心是while循环只要当前金额还没达到目标就继续滚一年。每次循环当前金额乘以1年利率年数加1。退出循环后years就是达到目标所需的最少年数。8.3 为什么用while而不是for这个题用for循环写会别扭因为你事先不知道要循环多少次。你可能用几年达到目标这取决于收益率和目标倍率的关系。收益率5%的情况下要翻倍需要大约14到15年因为1.05的14次方约等于1.9815次方约2.08。但如果是3%的收益率翻倍需要差不多24年。不同的输入参数的迭代次数差别很大这正是while循环的用武之地——条件式循环退出时机由计算过程动态决定。8.4 防止死循环while的经典风险写while循环最怕的就是死循环。这个题目里如果用户输入的收益率是0或者负数永远也不会达到目标金额循环会一直跑下去。所以必须有输入校验if annual_rate 0: print(年化收益率必须大于0) exit()另一个隐蔽的问题是如果你把current更新语句放错了位置或漏写了也会导致死循环。比如忘了加years 1这一行年份不增长但金额在增长——这个循环最后可能还是会退出因为你没靠years做条件但输出结果中years永远停留在一个值上逻辑就错了。或者你把current * (1 annual_rate)这行写成current principal * (1 annual_rate)那么current每次循环都被重置为本金乘以年利率永远不会增长dead loop直接卡死。所以写while循环之前先问自己三个问题循环条件是什么循环体里哪些变量的变化会影响这个条件退出时结果是正确的吗想清楚了再写基本不会翻车。8.5 进一步扩展定投场景和不连续收益这道题的进阶版可能是“定投”每年年初追加一笔固定金额问多少年能到目标。这其实只是把本金变动从单一变量变成了“每年都增加”current principal years 0 annual_contribution float(input(请输入每年追加投资金额)) while current target: current * (1 annual_rate) current annual_contribution years 1注意这题的边界条件会变成如果年化收益率为0但是每年有定投也是会达到目标的如果收益率和定投都为0就死循环。所以校验逻辑需要更全面。如果在真实场景中收益率不是固定值而是随年份变化的数组你还可以用for循环配合索引来模拟。这个扩展思路大家可以自己去尝试。9. 常见问题与排查技巧实录9.1 问题速查表我在批改和辅导中总结了这份速查表覆盖这个实验几乎所有典型报错典型症状可能原因排查思路与解决方式程序没有输出或输出为空for循环的range范围写错了循环体未执行检查range起点和终点检查是否有退出函数提前结束了程序输出的斐波那契数列缺少数值循环边界少算了一次用n3手动推演一遍循环过程单词数统计比预期少1最后一个单词后没有标点也没有空格被忽略了循环结束后检查in_word状态若为True则补一次计数密码判断永远输出“不合格”条件全部都用了and但某个标志位始终为False在循环里加print调试标志位的值e的计算结果与math.e相差大循环次数太少或精度阈值设置太大把阈值改小为1e-10或增加迭代次数折旧计算最后出现负的账面净值前几年折旧过猛没有做“平均摊销”切换加入剩余价值除以剩余年限的判断逻辑投资年数计算永远运行不止年利率输入为0或负数或本金更新语句写错增加输入校验确认current更新语句在循环体内销售统计的字典KeyError没有判断商品名称是否已存在使用defaultdict(float)或先if判断再更新9.2 调试利器print大法和手动推演很多同学写循环出bug的时候第一反应是“我代码肯定没问题”然后在那边干瞪眼。其实最高效的调试方式一直被低估了——print大法。在循环体里临时加一行print(当前i, i, 当前值, current)一运行你就知道循环到底跑了多少次、每次变量怎么变的问题基本一眼就能看出来。我经常会在实验课上跟学生说当你觉得循环逻辑怎么也想不通的时候就用n等于3或者5这样的小参数在自己的草稿纸上把每一次循环的变量变化写出来。这是最笨但也是最有效的方法。程序是机器的思维但你推演一遍就是人的思维两边一对齐bug就出来了。9.3 代码风格与命名建议最后说一点代码规范。很多同学写循环爱用i、j、k这种无意义命名的循环变量在纯计数场景下没问题但在这个实验里很多循环变量是有业务含义的建议用有意义的命名比如year、n、attempt、index。原因很简单过两周你再打开自己的实验报告看到for i in range(life)你会想不起来i代表什么看到for year in range(1, life1)一目了然。Python官方的PEP 8风格指南要求变量名用下划线分隔小写单词比如book_value而不是bookValue。虽然实验课一般不会因为命名扣分但养成好的习惯对后续写大规模代码非常重要。另外每一题的代码开头建议加两三行注释写明这个程序的功能和关键变量含义。注释不是写给老师看的是写给一周后的自己看的。写注释的过程也是在强迫你自己梳理逻辑很多bug在写注释的过程中就被发现了这招我屡试不爽。9.4 循环嵌套和break/continue的误区这个实验的题目基本都用不到嵌套循环但第六题如果在更复杂的数据结构上操作可能就会用到嵌套。一旦循环嵌套要注意break和continue只能作用于它所在的那一层循环break只能跳出当前的for或while不能跳出外层循环。这个知识点主要是防止你在后续项目里踩坑这里是打个预防针。continue的作用是跳过本次循环的剩余部分直接进入下一次循环。在密码判断和单词统计这两道题里如果你用continue来处理特殊字符务必确认continue放在循环体的末尾位置。如果continue放在循环体中间后面的代码会被直接跳过很容易漏掉一些标志位的更新。10. 一些值得记录的实操心得10.1 从第1关到第7关循环的“三个层次”做完整个实验之后如果你回头再看会发现这七道题其实在训练三个递进的层次第一层是计数循环斐波那契、密码判断你知道循环次数用for第二层是条件循环e的计算、投资年限你不知道循环次数但知道退出条件用while第三层是结构映射销售统计、折旧计算循环不再只是重复某段动作而是在对一组数据进行转换和累积这时候你的思路要从“我循环了多少次”转换到“我每次循环处理了什么数据”。能把这个层次想明白你就不会出现在应该用while的地方用for、应该用for的地方用while的困惑了。10.2 我对这个实验的定位通往“会写代码”的分水岭作为带过很多届学生的老博主我基于个人实验经验确实觉得这个循环结构实验是所有基础实验中最重要的一次甚至比后续的函数、列表、字典等章节更关键。原因很简单循环是第一个真正要求你“像计算机一样思考”的点是你从“抄代码”到“写代码”的分水岭。如果你在完成这个实验时感觉还有点吃力不用慌这是正常的。我自己当年学循环结构时也曾因为一个while循环死循环到不得不按下CtrlC强制中断程序。关键在于你要多写、多调试、多推演不要满足于看懂了别人的代码——看懂和能自己写出来是两回事。你可以试着今天把每一道题都独立重写一遍不看任何参考资料写不出来就回到草稿纸上推演推演明白了再写这个过程本身就是最有效的学习。最后送大家一个小建议做完这七道题之后去网上找几道“循环结构练习题”或者“经典算法100例”比如水仙花数、素数判断、最大公约数、猴子吃桃这些问题用循环实现一遍。等你能够不假思索地写出这些经典题目的循环解法的时候循环结构这一章你就算真正吃透了。

相关新闻

FraGAT+:基于分子片段的多尺度图注意力机制提升分子性质预测性能

FraGAT+:基于分子片段的多尺度图注意力机制提升分子性质预测性能

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 7:17:53 阅读更多 →
Linux NFS根文件系统挂载失败排查指南

Linux NFS根文件系统挂载失败排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 7:17:53 阅读更多 →
MR25H40CDF与STM32F405RG组合:工业级非易失存储方案落地

MR25H40CDF与STM32F405RG组合:工业级非易失存储方案落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 7:17:53 阅读更多 →

最新新闻

校园拼车系统实战:Java + 微信小程序全栈开发与高并发避坑指南

校园拼车系统实战:Java + 微信小程序全栈开发与高并发避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 8:30:46 阅读更多 →
UE4场景采集2D反射效果搭建与性能优化实战

UE4场景采集2D反射效果搭建与性能优化实战

1. 先搞清楚场景采集2D到底是个什么东西1.1 场景采集2D的工作原理做UE4项目做过反射效果的朋友应该都懂,反射这件事看着简单,做起来全是坑。尤其是做光滑地面、镜面墙、水面倒影这类需求时,引擎自带的反射方案要么效果凑合,要么性…

2026/10/4 8:30:46 阅读更多 →
openrig开源自动绑定全流程解析:从骨架生成到权重修正的实战指南

openrig开源自动绑定全流程解析:从骨架生成到权重修正的实战指南

做三维角色动画的人都清楚,绑定(Rigging)是整个流程里最磨人的一环。模型师交过来一个精美的角色,动画师等着要控制器开工,绑定要是做不好,后面全是返工。我刚入行那几年都是手工绑定,双足角色从…

2026/10/4 8:30:46 阅读更多 →
CANoe Basic Diagnostics ECU模板:UDS诊断测试从零到自动化

CANoe Basic Diagnostics ECU模板:UDS诊断测试从零到自动化

做ECU诊断测试这些年,我见过太多人第一次打开CANoe的诊断窗口就懵在原地的场景——一边是密密麻麻的总线报文,一边是手册里一行行的UDS服务定义,两者怎么也对应不上。其实如果你想快速搞懂UDS诊断流程,或者在没有真实控制器的情况…

2026/10/4 8:30:46 阅读更多 →
BL55072A段码LCD驱动开发:初始化、映射与低功耗设计

BL55072A段码LCD驱动开发:初始化、映射与低功耗设计

1. BL55072A这颗芯片值不值得用:定位与特性盘点做段码LCD驱动开发的朋友,肯定绕不开一个问题:液晶屏要显示数字和图标,到底该选什么样的驱动方案?板子上MCU资源还算宽裕,直接拿GPIO去扫COM和SEG行不行&…

2026/10/4 8:30:46 阅读更多 →
Qwen Image2.1高分辨率角色设定图一致性生成实战

Qwen Image2.1高分辨率角色设定图一致性生成实战

1. 为什么我要折腾 qwen image2.1 的高分辨率一致性生成先说结论:qwen image2.1 这个底模,在角色身份设定图这个细分场景里,潜力被严重低估了。大部分人拿它跑单张图,觉得效果还行就收工了,但只要你在 ComfyUI 里把工作…

2026/10/4 8:29:45 阅读更多 →

日新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/2 10:36:31 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/3 9:42:35 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/3 9:42:36 阅读更多 →