Python入门必练:用学生成绩统计程序打通编程基本功
1. 练习设计思路与目标拆解1.1 这道练习到底在练什么“课上练习1”听起来朴实无华但恰恰是这种不带任何修饰的标题最能看出一个学习者的真实状态——这是某编程入门课程的第一道完整实训题。我当初拿到类似题目时第一反应是“这不就一个小程序嘛”结果真正动手才发现越简单的题目越能暴露基本功的漏洞。这类课上练习通常不会限定具体业务场景而是给一个开放性的小需求让你把前面学过的变量、数据类型、条件判断、循环、函数这些零散知识点串起来。说白了它就是一座桥把“我学过Python”和“我能用Python解决问题”这两件事连接起来。很多同学栽跟头不是因为题目难而是因为第一次面对“从零到一”的完整流程分析需求、设计数据结构、写代码、调试、重构。我拿到的版本是“实现一个简易学生成绩统计程序”要求能录入若干学生的姓名和成绩然后输出最高分、最低分、平均分并且按照成绩高低排序展示。功能听起来不值一提但它覆盖了几乎所有Python入门阶段的核心语法点作为第一个综合练习性价比非常高。1.2 为什么选“学生成绩统计”作为练习载体接触过教学设计的开发者都知道第一道综合练习的选题非常讲究。太简单了比如只让写个“Hello World”学生练不到逻辑组织能力太难了比如直接做个爬虫或小游戏又会把初学者劝退。学生成绩统计这个场景正好卡在中间——它足够日常每个人学生时代都经历过它足够完整能覆盖“数据输入—数据存储—数据处理—结果输出”的完整链路它还足够灵活可以不断扩展需求来增加难度。从教学角度来说这个场景最容易让学生产生“代码有用”的直观感受。你写一段程序输入几个成绩它就能告诉你谁考得最好、谁需要补课、整体水平怎么样。这种即时反馈带来的成就感是刷一百道选择题都换不来的。而且成绩这个数据非常规整不会出现文件读写、网络请求这些初学者还搞不定的复杂情况可以把全部注意力集中在逻辑本身。我后来在指导某位同学做这个练习时发现他做完基础功能之后顺手加了一个“成绩等级分布统计”优、良、中、及格、不及格的各有多少人我特别鼓励这种做法因为这表明他开始主动思考“用户还需要什么”而不是被题目推着走。这种从被动完成到主动设计的转变才是这类练习最想培养的能力。1.3 完成这道练习需要掌握的基础知识清单在动手之前我建议你先对照一下这张清单缺哪块就补哪块别等到写代码写到一半才发现自己连列表切片都没搞明白。知识点具体要求常见误区变量与赋值能正确命名变量理解不同类型的内存表现把变量名起成a、b、c回头自己都看不懂数据类型区分int、float、str知道类型转换的时机忘记把input()的结果转成数字列表与字典掌握增删改查理解列表嵌套字典的结构用并列的单独变量存储大量数据条件判断if/elif/else的正确嵌套理解边界条件忘记处理空列表、负数等异常情况循环遍历for循环遍历列表理解range的用法混淆循环变量和列表元素的区别函数定义会用def封装功能模块理解参数与返回值把所有代码堆在全局不写函数看起来每一项都学过但综合起来用的时候很多同学就会出现“每个知识点都认识合在一起就不会用”的窘境。这很正常就像你学会了踩油门、踩离合、打方向盘但第一次上路还是手忙脚乱。课上练习1存在的意义就是给你一个安全的“驾校场地”让你在低风险环境下把整套动作串联起来。2. 核心知识点逐个击破2.1 输入输出的坑input()拿到的永远是字符串第一个让初学者集体翻车的地方几乎都出在输入处理上。Python的input()函数有一个反直觉的行为那就是不管你在控制台输入什么它返回的一定是字符串类型。你以为你输入的是数字85但Python拿到的是字符串“85”。这两个东西看起来差不多但一旦你拿去做数学运算报错信息会立刻糊你一脸。# 错误的示范 score input(请输入成绩) if score 60: # 报错TypeError字符串不能和整数比较 print(及格了)正确做法是在拿到输入之后立刻做类型转换而且要做异常处理因为你永远不知道用户会敲进来什么鬼东西。有人会输入“八十五”有人会直接按回车有人会输一个负数。这些情况都要在代码里提前做好防御。# 正确的示范 try: score float(input(请输入成绩)) except ValueError: print(输入无效请输入数字) continue # 如果是在循环里提示后重新输入这里我强烈建议用float而不是int来接收成绩。一方面考试成绩可能存在小数比如体育测评的8.5分另一方面float能兼容整数输入而int会直接把“85.5”这种合法输入给拒掉。我见过太多同学用int导致程序崩溃最后只能在输入格式上做严格限制——那是把麻烦转嫁给了用户不优雅。2.2 列表与字典数据怎么组织才不乱第二个容易翻车的地方是数据组织方式。成绩统计这个场景里你要处理的数据是“一组学生的姓名和成绩”。面对这样的数据直觉上有两种存法一种是两个平行列表名字放一个列表成绩放另一个列表通过下标对应另一种是列表套字典每个学生是一个字典包含姓名和成绩两个键。我明确建议用第二种也就是列表套字典的结构。原因有两个第一它的可读性更强student[name]比names[i]直观得多读代码的人不需要去数下标对应关系第二它的扩展性更好如果后面要加“学号”“班级”“是否补考”这些字段字典结构只需要增加键值对平行列表就得新增一个列表并维护下标同步非常痛苦。# 推荐的数据结构 students [] students.append({name: 张三, score: 88.5}) students.append({name: 李四, score: 59.0}) students.append({name: 王五, score: 92.0})有一个细节处理需要注意往列表里追加字典时如果你先定义了一个字典变量然后在循环里反复修改它再追加就会遇到“所有字典都一样”的经典bug。因为列表里存的是字典的引用不是副本。我当初排查这个问题花了整整一个晚上最后才发现每次追加之前需要student {}重新初始化或者直接在同一行用字面量构造新字典。2.3 循环与条件让程序学会“判断”和“重复”这两个语法点本身不难难的是组合起来解决实际问题。在成绩统计场景里循环和条件的配合至少出现在三个地方录入时的重复输入、统计时的逐个判断、排序时的交换逻辑。录入环节最推荐用while True break的结构让用户输入“q”表示结束录入。这样用户不需要事先告诉程序“我要录几个人”程序也能灵活应对不同数量的数据。判断结束条件时一定要放在类型转换之前否则用户输入“q”就会被float()强行转换然后报错。while True: name input(请输入姓名输入q结束) if name q: break score_input input(请输入成绩) if score_input q: break score float(score_input) students.append({name: name, score: score})统计最高分、最低分、平均分的时候如果你不想用内置的max()、min()、sum()也可以手写循环来遍历。虽然手写效率低但作为教学练习我反而建议初学者至少手写一遍搞清楚原理之后再用内置函数。这就好比学算术先学列竖式后面再用计算器才明白它到底算了什么。2.4 函数的封装思想从“能跑”到“能看”很多同学的第一个版本是一坨几百行的顺序代码功能全实现了但谁看了都头疼。这时候就需要用函数把它拆开。拆分的标准很简单一块代码如果承担了一个独立的任务就把它扔进一个函数。录入是一件事统计是一件事排序展示是另外一件事。def add_student(students): # 负责录入一个学生信息并追加到列表 def calculate_stats(students): # 负责计算最高分、最低分、平均分 def display_ranking(students): # 负责按成绩排序并展示main函数里只放主流程逻辑调用这几个子函数。这样做的第一个好处是出错时定位方便你只需要检查出错的那个函数内部就行不用在一坨代码里用肉眼扫描第二个好处是逻辑清晰别人看你的代码先看main函数就能知道你整个程序做了什么。我见过有同学为了追求代码精简把所有功能硬塞在一个函数里用一堆if参数去区分“现在是录入模式还是统计模式”结果代码的行数没少多少理解难度倒翻了好几倍。适度拆分才是正解过度设计也是另一种坑。3. 完整实现与踩坑实录3.1 第一版先让程序能跑起来如果你完全没有头绪那就先别管代码好不好看先把最核心的流程写出来再说。我的习惯是先画一个最简单的流程图开始→录入学生→循环判断是否继续→统计三项指标→排序→展示结果→结束。然后照着这个流程一步步填空。第一版我的代码大概是这样的简化版students [] # 录入阶段 while True: name input(请输入姓名q退出) if name q: break score float(input(请输入成绩)) students.append({name: name, score: score}) # 统计阶段 scores [s[score] for s in students] max_score max(scores) min_score min(scores) avg_score sum(scores) / len(scores) print(f最高分{max_score}) print(f最低分{min_score}) print(f平均分{avg_score}) # 排序展示阶段 sorted_students sorted(students, keylambda x: x[score], reverseTrue) for i, stu in enumerate(sorted_students, 1): print(f第{i}名{stu[name]} {stu[score]}分)这里面有几个很关键的细节。排序用了sorted()lambda这个写法是Python处理“按字典的某个键排序”最简洁的方式千万要记住。lambda x: x[score]表示“对列表中的每个元素x按x[score]这个值来排序”。如果不加这个参数Python会尝试直接比较两个字典的大小然后给你一个TypeError: not supported between instances of dict and dict。还有列表推导式[s[score] for s in students]如果你第一次见可能觉得有点玄乎。它其实就是一个简写版的循环把students里的每个学生s取出来然后提取它的score值放进一个新的列表。看不懂的话可以先用普通循环写等用熟了再换成推导式两者结果一样。3.2 第二版补上异常处理和边界条件第一版能跑但只适用于所有人都规规矩矩输入数字的“理想世界”。真实场景里用户一定会犯错。我在测试的时候就专门试了各种奇怪的输入直接按回车、输入字母、输入负数、输入1000分。每一种非预期输入都有可能让程序直接崩溃。第二版的主要工作就是给这些边界情况打补丁。录入循环里要加入异常处理成绩的有效范围要校验0到100除非你想做加分题支持102分的特殊成绩用户中途想退出也要有明确的操作路径。我还加了一个小功能最后问用户“是否还要继续录入其他班级的数据”让整个程序可以连续处理多组数据。while True: raw_input input(请输入姓名q退出) if raw_input.lower() q: break if not raw_input.strip(): print(姓名不能为空) continue name raw_input.strip() try: score float(input(请输入成绩)) except ValueError: print(成绩必须是数字) continue if score 0 or score 100: print(成绩必须在0到100之间) continue students.append({name: name, score: score})raw_input.strip()这行是为了处理用户输入时误带空格的情况。比如用户输入“张三 ”如果不去掉末尾空格后面排序展示时会显示“张三 ”而不是“张三”虽然不影响排序结果但看着很难受。这种小细节很容易被忽略但用户实际使用时却能直接感知到粗糙感。3.3 第三版从“能用”到“好用”的代码走查当功能完全稳定之后我开始做代码走查也就是重新通读一遍自己的代码问自己几个问题变量名是否表意清楚每个函数是否只做一件事有没有重复代码可以提取异常处理是否覆盖了所有用户操作路径这个阶段最大的收获得益于一个习惯写完之后隔一天再回来看自己的代码。当时觉得天衣无缝的实现隔一天再看通常会挑出一堆毛病。我列一个第三版清理过的问题清单变量名命名把sco改成score_input把stu在非循环场景下改成student让代码读起来像话。函数拆分把录入、校验、统计拆成独立函数主流程只保留调用逻辑。魔法数字成绩上限100下限0这种魔法数字应该定义为模块级常量MAX_SCORE 100、MIN_SCORE 0后续要调整只需要改一行。交互提示所有输入提示都统一带上默认答案的提示比如“q退出”降低用户试错成本。MIN_SCORE 0 MAX_SCORE 100 EXIT_FLAG q def get_score(): while True: try: score float(input(请输入成绩)) except ValueError: print(成绩必须是数字请重新输入) continue if MIN_SCORE score MAX_SCORE: return score print(f成绩必须在{MIN_SCORE}到{MAX_SCORE}之间)这个get_score()函数里用了return来结束死循环这是Python里非常常用的一个模式用while True构造无限循环用return从满足条件的那个位置干净退出。对比有些同学用flag变量加上一堆if flag:判断来跳出循环这种直接return的写法明显更清爽。4. 常见问题排查与调试技巧4.1 运行报错速查表我把这个练习里最常见的运行报错整理成一个速查表如果你遇到了下面的问题直接对照查找解决方案报错信息出现原因解决方案TypeError: not supported between instances of str and floatinput()返回的字符串没转数字就参与了比较在录入时用float()转换并加异常捕获TypeError: not supported between instances of dict and dict调用sorted()时没指定排序key使用sorted(students, keylambda x: x[score])ValueError: could not convert string to float: q用户输入q退出时先做了类型转换先判断是否为退出关键字再做转换ZeroDivisionError: division by zero一个学生都没录入就计算平均值统计前判断列表是否为空KeyError: score字典里没有这个键检查构造字典的字段名是否拼写一致我见过scorce拼错的最后一个KeyError是我见过最隐蔽的翻车点因为它不一定是运行时报错有时候隐藏在一个逻辑分支里只有特定条件下才会触发。有个同学录了三个学生没有报错录到第四个人的时候突然KeyError后来发现他在某个分支里构造字典时写的是{scores: 88}跟其他分支的{score: 88}不一致。4.2 逻辑bug的定位方法print大法运行报错容易解决因为它会告诉你出错的位置和原因。真正让人血压飙升的是逻辑bug程序从头到尾不报错但结果就是不对。比如输入三人的成绩分别是80、90、70排序结果却是70、90、80。这种时候查错的方法听起来非常笨但效率极高那就是在关键位置打印中间结果。我调试这类小练习的逻辑bug基本都会在几个节点加上临时的print# 调试时加的打印 print(当前学生列表, students) print(提取出的分数列表, scores) print(排序后的列表, sorted_students)看到中间结果之后问题通常一眼就能发现。比如我曾经遇到一个情况是students列表里有8个人但scores列表只有5个成绩一打印中间结果就发现原来有两名学生的score字段被意外覆盖成了空字符串float()转换时静默失败导致那个元素根本没加入scores列表。还有人会遇到“排序总是反的”的问题那是因为他把reverseTrue写成了reverseFalse或者是把降序理解错了。看到打印结果后默默改过来就好。自己用print方法排查过一次之后你对数据在程序里的流动过程就会形成肌肉记忆以后遇到再复杂的bug也能快速定位。4.3 用切片和索引避免“差一错误”初学者在排序展示的时候经常遇到“差一错误”off-by-one error。比如用for i in range(len(students))遍历然后输出students[i1]结果最后一个元素直接IndexError或者排序后要显示“前三名”用sorted_students[:2]切片结果只显示了两名。这里分享两个实用经验。第一遍历时如果不需要下标就用for stu in students而不是for i in range(len(students))从源头避免越界。第二切片[:n]的含义是“取前n个元素”取前三名要写[:3]而不是[:2]。我找个笨办法帮自己记[:3]的意思是“从开头取到下标3的位置”Python切片是左闭右开区间所以[:3]正好是下标0、1、2这三个元素。还有一个细节如果你想同时拿到“名次”和“学生信息”建议用enumerate而不是自己维护计数器。看下面两种写法你会发现enumerate版本简洁得多# 自己维护计数器 rank 1 for stu in sorted_students: print(rank, stu[name], stu[score]) rank 1 # 用enumerate for rank, stu in enumerate(sorted_students, start1): print(rank, stu[name], stu[score])5. 练习之外的延伸思考与扩展方向5.1 为什么“课上练习1”值得认真对待我见过太多学习者在这类基础练习上敷衍了事觉得“功能都实现了交上去就行”。但我要说正是这种心态拉开了不同学习者之间后来的差距。课上练习1是你第一次有机会体验完整的开发流程虽然步骤简单但这个流程——分析问题、设计数据结构、编码实现、测试调试、代码整理——和做大型项目的流程是高度一致的。认真打磨一个简单练习和囫囵吞枣做完十个简单练习效果完全不一样。前者培养的是“把代码写干净”的肌肉记忆后者只是在浪费时间。我建议你把代码打印出来用红笔圈出每一处你觉得可以改进的地方这个动作能帮你建立一个很重要的能力批判性地阅读自己的代码。5.2 给这个练习加需求的三个方向如果你把这个基础版本写完了还想再练手我强烈建议你在三个方向上扩展。第一个方向是存储升级把数据保存到本地文件里程序关闭后再打开还能接着录入。这需要用到文件读写会打开一扇新世界的大门。第二个方向是分析升级增加成绩分段统计、计算中位数和标准差让程序从“统计工具”变成“分析工具”def analyze_distribution(students): levels {优秀: 0, 良好: 0, 中等: 0, 及格: 0, 不及格: 0} for stu in students: score stu[score] if score 90: levels[优秀] 1 elif score 80: levels[良好] 1 elif score 70: levels[中等] 1 elif score 60: levels[及格] 1 else: levels[不及格] 1 return levels第三个方向是交互升级给程序加上一个简单的菜单让用户选择“录入”“统计”“排序展示”“退出”这会让程序更像一个真实可用的系统。选一个方向做深比三个方向都浅尝辄止要好得多。5.3 我做完这个练习后的三个心得第一代码是写给下一个开发者看的而那个“下一个开发者”往往就是两周后的自己。如果你现在看到自己的代码觉得陌生说明变量命名和函数拆分做得不够好。花时间把代码整理到“别人也能轻松看懂”的程度这不是浪费时间是在给自己节省未来的时间。第二空列表和多用户输入的边界情况一定要从第一版就开始处理。不要觉得“现在只是练习不需要考虑这么多”。习惯是养出来的从练习就开始防御式编程以后做真实项目就不会在测试阶段被一堆边界case逼疯。第三做练习时一定要自己亲手调试每一个报错。我之前带过的几位同学遇到报错第一反应就是把代码发给我问“为什么”。但我坚持让他们自己先用print看数据、用错误信息里的行号去定位实在卡住了我再给提示。为什么因为调试能力是独立开发最核心的技能之一这个能力只能靠亲自踩坑来获得别人替你做不了。课上练习1就是一个低成本踩坑的好机会——在真实项目里踩坑有工期压力在练习里踩坑只有成长。从“课上练习1”这个小标题出发能延伸出来的东西其实比想象中多得多。这也是为什么我一直建议初学者别眼高手低把一个简单的练习做透学到的东西远不止题目本身那点语法知识。现在就可以打开编辑器把这份代码敲一遍再试着按上面的思路改一版你会有意想不到的收获。