第 4 章 · M1 活动报名基础版
循环与批量报名
第 3 章的判定器能给一名申请者明确结果;面对多份申请,还需要让同一套规则重复运行,并准确记录人数与实收金额。本章把循环、分支和状态更新合成你的第一个个人项目里程碑。
- 预计阅读
- 65 分钟
- 上机实践
- 90 分钟
- 前置知识
- 第 1—3 章
- 本章产出
- 个人 M1 报名基础版
学完本章,你应该能够
- 说明一轮循环、循环体、结束条件与状态更新分别是什么。
- 用
for和range()按计划次数处理申请,避免漏掉最后一份。 - 用
while表达条件控制的重复,并指出循环如何终止。 - 区分计数器与累加器,在正确分支更新报名人数和实收金额。
- 解释
break与continue的不同,并用跟踪表核对多轮状态。 - 独立完成 M1 程序、三组测试和一次限时修改。
4.1 从处理一人到处理多人
假设迎新活动收到 30 份申请。把第 3 章的代码复制 30 遍,看似可以工作,却会带来三个问题:程序冗长;规则一旦变化,容易漏改某一份;申请数变动时又得重新复制。循环(loop)让一段缩进代码按规则重复执行。循环体完整执行一次,叫作一轮或一次迭代(iteration)。
写循环前,先问四件事:重复什么任务?预定多少轮或何时结束?每轮哪些数据会变化?什么变化保证最终停下?本项目的答案是“每份申请输入、判定一次;最多处理本次申请人数;成功时增加已报名人数;处理完计划轮数或名额已满时结束”。
| 阶段 | M1 程序中的工作 |
|---|---|
| 循环前 | 读取活动初始数据;把成功数、失败数和实收金额设为 0 |
| 每轮 | 先查名额,再读取一名申请者,沿用身份与费用规则,按结果更新状态 |
| 循环后 | 计算剩余名额,只输出一次最终汇总 |
我们只统计成功、失败人数和金额,不保存每个申请者的姓名。名单保存要等第 6 章学习列表;本章也不使用文件或异常处理。
4.2 for:按顺序重复循环体
当计划处理多少份申请已经知道时,for 很合适。它每轮从 range() 提供的整数中取一个值,交给循环变量,再执行一次缩进的循环体。
for sequence in range(1, 4):
print(f"第{sequence}份申请")
print("正在检查")
print("本批次结束")第1份申请 正在检查 第2份申请 正在检查 第3份申请 正在检查 本批次结束
先预测“本批次结束”会打印几次:它与 for 对齐,不属于缩进循环体,所以只打印一次。若把最终汇总缩进进去,就会每轮都输出一份尚未完成的报告。循环变量 sequence 在三轮中依次取 1、2、3。
计划循环三轮,只说明最多检查三份申请;不代表三人都会报名成功。失败申请不占名额,满额时还可能提前结束。
4.3 range():包含开始,不包含停止
range(stop) 从 0 开始,到 stop 之前结束;range(start, stop) 从 start 开始,仍不包含 stop。它提供的是可按顺序取出的整数,不要求先创建列表。
for number in range(3):
print(number)
print("---")
for sequence in range(1, 4):
print(sequence)0 1 2 --- 1 2 3
若本次有 application_count 份申请,编号从 1 到它本身,停止值就要写成 application_count + 1。多写的 1 不是多处理一人,而是补偿“不包含停止值”。
application_count = 3
for sequence in range(1, application_count + 1):
print(f"申请 {sequence}")申请 1 申请 2 申请 3
若误写 range(1, application_count),第 3 份会漏掉。这种少一轮或多一轮的现象叫“差一错误”。当申请人数为 0 时,range(1, 1) 没有可取的数,循环体一次也不执行;这是后面必须测试的边界。
4.4 计数器与累加器:把多轮结果留下来
状态(state)是某一时刻影响后续判定和最终报告的数据。本项目至少有四项状态:当前已报名人数、成功人数、失败人数、实收金额。计数器记录事件发生的次数,通常每次加 1;累加器汇总每轮产生的数值,增加的量不一定相同。
success_count = 0
total_collected = 0.0
success_count = success_count + 1
total_collected = total_collected + 20.0
success_count = success_count + 1
total_collected = total_collected + 25.0
print(success_count)
print(f"{total_collected:.2f}")2 45.00
两个成功事件让计数器从 0 变成 2;两笔不同金额让累加器变成 45.00。初始化必须在循环前做一次;若把 success_count = 0 放进循环体,下一轮会抹掉上一轮的记录。
| 本轮结果 | registered | success_count | failure_count | total_collected |
|---|---|---|---|---|
| 身份失败 | 不变 | 不变 | 加 1 | 不变 |
| 费用失败 | 不变 | 不变 | 加 1 | 不变 |
| 报名成功 | 加 1 | 加 1 | 不变 | 加本轮已付金额 |
注意“已报名人数”包含运行前已经报上的人;“成功人数”只统计这一批新成功的人。两者不应被当成同一个数。
4.5 while:由条件决定下一轮
当次数未知、但结束条件明确时,可以使用 while。每次准备进入循环体之前,解释器都会重新计算条件。设计时要看清初始化、条件、循环体和状态更新四个位置。
round_number = 1
while round_number <= 3:
print(f"第{round_number}轮")
round_number = round_number + 1
print("结束")第1轮 第2轮 第3轮 结束
第 3 轮末尾,round_number 变成 4;下一次检查 4 <= 3 为假,循环才停。若忘记更新,条件会持续为真,可能形成无限循环;不要在在线运行器中直接尝试故意不更新的版本。
不知道人数时,用约定的结束信号
下面把 q 约定为“停止输入”的哨兵值。第一次检查条件前必须先读一次;每轮末尾还要再次读取,否则条件永远检查旧值。
student_name = input("姓名(q结束):")
while student_name != "q":
print(f"已接收:{student_name}")
student_name = input("姓名(q结束):")
print("受理结束")姓名(q结束):小林 已接收:小林 姓名(q结束):小周 已接收:小周 姓名(q结束):q 受理结束
这只是理解 while 的独立小例子;M1 基础版仍用已知申请人数的 for。真实姓名若可能就是 q,就不能随意把它当结束信号;本周将其作为约定好的练习输入。
4.6 break 与 continue:停下还是跳过
两者都改变正常的逐行执行,但范围不同:break 立即离开整个循环;continue 只跳过本轮剩余语句,开始下一轮。它们通常与 if 配合使用。
for sequence in range(1, 5):
if sequence == 3:
print("停止受理")
break
print(f"处理第{sequence}份")
print("输出汇总")处理第1份 处理第2份 停止受理 输出汇总
for sequence in range(1, 4):
if sequence == 2:
print("第2份无效,跳过本轮")
continue
print(f"处理第{sequence}份")
print("输出汇总")处理第1份 第2份无效,跳过本轮 处理第3份 输出汇总
M1 中,若本轮开始就已满额,继续询问姓名和金额没有意义,应在读取申请前 break。continue 可以用于进阶版跳过格式无效的一份申请,但要先记录无效输入;它不是“悄悄不统计失败”的借口。
continue 后面的更新可能被跳过如果在 while 中,推进结束条件的语句写在 continue 后面,它可能永远得不到执行,造成死循环。因此先明确每轮哪些状态必须更新,再决定是否跳过。
4.7 用循环跟踪表检查状态
只看最后报告,难以发现哪一轮把状态更新错了。循环跟踪表把“进入本轮前—本轮结果—更新后”排在同一行。下面设容量为 3、初始已报名 1 人、活动费 20 元,计划处理 4 份申请。
| 计划轮次 | 处理前报名 | 本轮发生什么 | 处理后报名 | 成功 / 失败 | 实收 |
|---|---|---|---|---|---|
| 1 | 1 | A:本校、付足,成功 | 2 | 1 / 0 | 20 |
| 2 | 2 | B:非本校,失败 | 2 | 1 / 1 | 20 |
| 3 | 2 | C:本校、付足,成功 | 3 | 2 / 1 | 40 |
| 4 | 3 | 先发现满额,break | 3 | 2 / 1 | 40 |
计划有四轮,真正读取了三个人;第四轮还没问姓名就停了。结果是成功 2、失败 1、剩余 0、实收 40。失败的 B 不改变已报名人数,也不增加实收。这个表就是课堂课件“状态跟踪”实验的书面版本。
4.8 把循环和第 3 章规则合成 M1
下面的完整基础版,先读取活动的四项初始数据,再初始化三个汇总变量。每轮先看名额;未满才读取一名申请者,依次检查身份与费用。只在成功路径更新 registered、成功数和实收金额。请保存为 batch_registration.py,从头运行。
# 第4章 M1 基础版:批量活动报名与汇总
capacity = int(input("活动容量:"))
registered = int(input("当前已报名人数:"))
activity_fee = float(input("每人活动费:"))
application_count = int(input("本次申请人数:"))
success_count = 0
failure_count = 0
total_collected = 0.0
for sequence in range(1, application_count + 1):
if registered >= capacity:
print("名额已满,停止受理")
break
print(f"\n第{sequence}份申请")
student_name = input("姓名:")
campus_status = input("是否本校学生(y/n):")
paid_amount = float(input("已支付金额:"))
if campus_status != "y":
failure_count = failure_count + 1
print(f"{student_name}报名失败:身份不符合")
elif paid_amount < activity_fee:
failure_count = failure_count + 1
print(f"{student_name}报名失败:费用不足")
else:
registered = registered + 1
success_count = success_count + 1
total_collected = total_collected + paid_amount
print(f"{student_name}报名成功")
remaining_places = capacity - registered
print("\n===== 报名汇总 =====")
print(f"成功人数:{success_count}")
print(f"失败人数:{failure_count}")
print(f"剩余名额:{remaining_places}")
print(f"实收金额:{total_collected:.2f} 元")活动容量:5 当前已报名人数:2 每人活动费:20 本次申请人数:3 第1份申请 姓名:A 是否本校学生(y/n):y 已支付金额:20 A报名成功 第2份申请 姓名:B 是否本校学生(y/n):n 已支付金额:20 B报名失败:身份不符合 第3份申请 姓名:C 是否本校学生(y/n):y 已支付金额:10 C报名失败:费用不足 ===== 报名汇总 ===== 成功人数:1 失败人数:2 剩余名额:2 实收金额:20.00 元
运行结果中的数字、姓名和 y/n 是示例键盘输入,不属于源代码。点击“在线运行”会先复制代码并打开新标签页;你仍需粘贴代码,再按提示逐项提供输入。下载文件与上方完整示例相同。
- 1初始数据
容量、当前已报名、每人费用、本次申请人数,分别转换为合适的数值类型。
- 2初始化状态
成功数、失败数和实收金额从零开始,只在循环前执行一次。
- 3先满额,后输入
第 3 章的满额规则仍是第一优先级;满足时直接结束,不向后面的申请者索取数据。
- 4本轮判定与更新
失败只增加失败数;成功占一个名额,并计入已付金额。
- 5循环后汇总
剩余名额由最终状态计算,最终报告只出现一次。
“满额 → 身份 → 费用 → 成功”的业务优先级没有变化;只是满额现在可以停止整批受理,身份与费用失败后仍继续下一份。不要为了循环而改变原有业务规则。
4.9 三组必测数据与真实排错
每组先写下预期,再独立运行一次完整程序,把实际值填进个人测试表。不能只用“第一人成功”证明批量处理正确;M1 必须覆盖混合路径、中途满额和零次申请。
| 测试 | 输入 | 预期汇总 | 检验点 |
|---|---|---|---|
| A · 混合 | 容量 5、初始 2、费 20、申请 3;A/y/20、B/n/20、C/y/10 | 成功 1、失败 2、剩余 2、实收 20.00 | 失败不占名额、不计金额 |
| B · 中途满额 | 容量 3、初始 2、费 20、申请 3;第一人 y/20 | 成功 1、失败 0、剩余 0、实收 20.00 | 第二轮开始就 break,不再读取姓名 |
| C · 零次申请 | 容量 5、初始 2、费 20、申请 0 | 成功 0、失败 0、剩余 3、实收 0.00 | 循环体零次执行,汇总仍出现一次 |
结果不对时,从第一处状态分叉查起
如果 A 的成功数是 1,但实收变成 50.00,检查是否在失败路径也累加了金额;若申请 3 人只出现两轮,检查 range() 的停止值;若 C 没有最终报告,检查汇总是否误缩进在循环体中。对于 while 不停的情况,检查参与条件的变量是否在每条继续路径上都能更新。
本版假定人数和费用输入有效,float() 遇到非数字文本仍会报错;它也不检查负数、超容量等异常初始数据。把这些情况记录为当前限制,不要把“程序没有处理”写成“测试通过”。
4.10 练习与 M1 个人验收
先预测,再看解释
- 读范围
range(2, 6)会给出哪些数?range(1, 3)能否处理第 3 份申请?查看答案
依次是 2、3、4、5;
range(1, 3)只有 1、2,会漏掉第 3 份。 - 读状态
容量 3、初始报名 1,第一人成功、第二人身份失败后,
registered与失败数各是多少?查看答案
registered从 1 变成 2,第二人失败不占名额;失败数为 1。 - 选控制语句
名额已满要停止整批受理;某份申请格式无效,但还要处理下一份。两处分别用什么?
查看答案
前者用
break,后者可用continue;跳过前要按自己设计的规则记录这份无效输入。 - 找终止条件
while round_number <= 3中若一直不改变round_number,会发生什么?查看答案
条件可能始终为真而无限循环。每轮应让控制变量朝不满足条件的方向更新,并检查跳过路径不会遗漏更新。
完成自己的活动报名基础版
每位学生独立维护、提交、测试并解释自己的 ClubHub 版本。示例用于理解结构;请自行选定活动情境与测试数据,并完成现场限时修改。临时同伴互测只提供反馈,不替代个人证据。
最低验收要求
- 提交
w04_学号_batch_registration.py,文件开头写姓名、学号和用途注释。 - 接收 4 项活动初始数据,循环读取每名申请者的姓名、身份和已付金额。
- 沿用第 3 章规则;满额时先检查再输入,成功与失败只更新各自应更新的状态。
- 只输出一次成功数、失败数、剩余名额和两位小数的实收金额。
- 提交三组“输入—预期—实际—结论”测试记录,能用跟踪表解释至少三轮状态变化。
- 完成个人限时修改:增加
cancelled_count;姓名输入q时加 1 并break,不再询问身份和金额,最终汇总显示提前结束次数。 - 保留限时修改的运行证据,并附 80—150 字错误复盘。
进阶可为不是 y/n 的身份输入单独计数,并用 continue 跳过本轮;挑战可用 while 与 q 重写受理过程。这些扩展应先写结束条件和测试预期,不属于基础版三组必测的替代品。
章末检查
- 我知道
range()不包含停止值,能解释+ 1的原因。 - 我把汇总变量初始化在循环前,把最终报告放在循环后。
- 我能解释失败申请为何不改变已报名人数与实收金额。
- 我能区分计划轮数、实际读取人数和成功人数。
- 我用三组测试和一次个人修改证明 M1 可运行、可解释。
延伸阅读:廖雪峰《Python教程》在线 PDF中的“循环”,以及菜鸟教程 Python 3 循环语句。本章用 ClubHub 的报名与汇总问题重新组织例子,以课程主数据的知识边界为准。