ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python新手第一次作业:CSV成绩统计程序的完整实战复盘

Python新手第一次作业:CSV成绩统计程序的完整实战复盘 1. 拿到“第一次作业”后我做的第一件事不是写代码大概每个编程新手都会经历这样一个时刻老师把题目往屏幕上一贴下面跟着一串示例输入输出然后教室安静三秒所有人脑子里同时冒出一句“这到底要我干嘛”。我当时的作业长这样读取一个 CSV 文件里面是一堆学生的姓名、学号和三次作业成绩最后输出平均分从高到低排序的名单并且要把不及格的标出来。题目本身不难但难的是“第一次”这三个字。第一次意味着你连开发环境都是刚装好的第一次意味着你还分不清“报错看不懂”和“代码没写完”哪个更让人崩溃第一次也意味着你根本不知道一份看起来能跑的作业离“合格”到底有多远。现在回过头看那次作业我前后写了大概三四天真正写代码的时间可能就半天剩下的时间全花在拆需求、查资料、改 bug、重读题目的循环里。这篇博文就把这个过程完整复盘一遍把我踩过的坑、绕过的弯、还有后来才想明白的设计思路都摊开讲。如果你也正卡在类似的第一次作业上希望这份记录能让你少走几步弯路。提示这篇文章的作业实例以 Python 实现一个成绩统计命令行工具为主但拆解需求、设计结构、调试排查的思路是通用的换成 C 语言、Java 或者 JavaScript 都能套用。2. 拆解题目才是第一道真正的题刚拿到作业的时候我差点直接打开文件就开始写。后来想了想题目里其实是藏了要求的只是不会有人主动给你划重点。2.1 把一句话题目拆成五条可执行清单我的作业原文大概是这样一段描述“编写一个程序读取 data.csv每个学生有三项成绩平时、期中、期末计算总评平时占百分之二十、期中占百分之三十、期末占百分之五十按总评降序输出并在总评低于 60 的姓名后面标注‘不合格’。最后把结果写到 result.txt。”看起来就一句话但我把它拆成了这些子任务读文件CSV 是什么格式、用什么函数读、文件读不到怎么办算成绩加权平均怎么算、浮点数精度要不要处理排序按总评降序那总评相同的怎么办标记低于 60 分加标注是跟在名字后面还是单独一列写文件编码格式用 UTF-8 还是 GBK写完之后要不要控制台也打印一份别小看这个拆解的过程。它本质上是一次“需求评审”只不过评审人是你自己。把模糊的题目变成可逐项打勾的步骤后面写的时候脑子就不会乱。我那会儿甚至拿纸画了一个流程读数据 → 清洗 → 计算 → 排序 → 标记 → 输出。画完之后才发现顺序很重要比如排序必须在标记之前不然标了“不合格”再排序会把这个字符串也带进去比较。2.2 先搞清楚“合格”长什么样作业题的末尾通常会写“提交要求”这一步很多人直接忽略了。我那次作业的要求是提交一个源代码文件和一个说明文档文档里写清楚运行方式。但后来听老师说全年级一百多份作业里将近三分之一的人没写运行方式——你让人家怎么跑拿着 Python 源码干瞪眼吗这就是第一次作业真正要教给你的东西完整交付。功能是对的只是及格线能让别人一分钟之内跑起来才是良好如果代码注释清楚、结构清晰、连异常情况都考虑到了那才是优秀。我当时也没意识到这一点直到后来做课程设计被老师要求补 README才慢慢养成“交付物要让人能直接用”的习惯。所以拿到任何作业先花十分钟确认三件事交什么文件、用什么格式、按什么标准验收。哪怕题目里没有写也要主动问老师或者看往年的提交模板。这十分钟省下的可能是你之后补交作业的整整一晚。3. 设计思路先想清楚再动手真不是浪费时间很多人第一次写作业最没耐心的就是设计阶段觉得“不就是几行代码吗”。我一开始也这么想直到在排序上栽了个跟头才明白先设计后编码其实是在给自己省 debug 的时间。3.1 用数据流图代替架构图不用画那种很唬人的系统架构图一个小作业而已。但你可以像我一样在草稿纸上画一个数据流图把输入、处理、输出三个环节画清楚。我当时画的流程是这样的输入data.csv 文件处理1逐行读取跳过表头处理2拆分字段做类型转换处理3十个学生的数据存成什么结构处理4按总评排序处理5生成带“不合格”标记的展示字符串输出result.txt 和控制台打印画完之后我意识到整个程序其实可以拆成五个小函数read_data、calculate_score、sort_students、format_output、write_result。主程序只需要把这五个函数按顺序串起来一共不超过二十行。这个认知对当时的我来说是很震撼的——原来代码不是一坨写完的而是拼积木一样拼出来的。3.2 选择数据结构字典还是列表存储学生的数据我想了两种方案用列表套列表[[name, id, score1, score2, score3], ...]好处是直观坏处是代码里到处都是 students[i][2] 这种魔法索引写多了自己都分不清哪个是哪个用列表套字典{name: ..., sid: ..., scores: [..], total: ...}好处是字段名一目了然坏处是稍微啰嗦我最后选了字典。理由是排序的时候用 keylambda s: s[total]读起来极其清晰后面格式化输出也方便。这种“多写几个键名换来终身可读性”的买卖怎么算都划算。3.3 提前想好“边界情况”设计阶段最容易漏掉的是边界情况。老师给的数据文件永远是规规矩矩的十个学生、三列成绩但如果你只针对“完美情况”写代码那你的程序就是个玻璃娃娃。我当时列了几个问题文件里空行怎么办学号带前导零怎么处理成绩缺失或者不是数字怎么办两个人的总评完全一样排序是否稳定前三个问题我当时没全解决但至少在设计里想到了后来实现的时候一个个补上了。这个意识很重要因为真实的程序跑在手里的数据上永远不如你想的那么干净。4. 核心实现与实操细节这一段我会把那次作业的最终实现拆开来讲包括每一步为什么这么写、参数为什么这么选、还有我踩过的具体坑。代码都是当时那个水平的谈不上优雅但足够诚实。4.1 读取 CSV不要硬编码路径我一开始写的是with open(data.csv, r, encodingutf-8) as f: lines f.readlines()这段代码在 PyCharm 里能跑通但换一个目录运行就崩溃。后来我知道这是因为工作目录的问题。更稳的做法是显式指定路径或者用相对于脚本文件的路径from pathlib import Path file_path Path(__file__).parent / data.csv注意Path(file).parent 取的是当前脚本所在目录而不是“执行命令时所在的目录”。这个技巧在很多小工具类项目里都管用尤其是你把脚本放到别的机器上跑的时候。如果一定要用内置的 csv 模块那就别手动 split(,)因为 CSV 里如果出现引号包裹的逗号手动 split 直接炸掉。作业数据可能没这么复杂但养成用 csv.reader 的习惯没有坏处。import csv rows [] with open(file_path, r, encodingutf-8) as f: reader csv.reader(f) header next(reader) # 跳过表头 for row in reader: if not row: # 跳过空行 continue rows.append(row)4.2 计算加权总评浮点数是第一个坑加权计算本身不难难在浮点数精度。比如平时 88.5 分期中考 71.3 分期末考 90.2 分总评算出来可能是 81.95000000000002。你的程序往 result.txt 里一写老师打开一看后面一堆小数印象分直接掉一半。解决的办法是保留两位小数。但要注意四舍五入的时机。我当时图省事在算每一项成绩的时候就 round结果导致最终总评有偏差。正确的做法是先算完整加权总和再对最终结果做 round。def calculate_total(scores, weights(0.2, 0.3, 0.5)): total sum(float(s) * w for s, w in zip(scores, weights)) return round(total, 2)这里还有个 Python 特有的坑round(2.675, 2) 的结果是 2.67不是 2.68。因为 2.675 在二进制浮点数里根本没有精确表示。如果老师特别抠这种细节可以用 Decimal但作业场景下 round 足够了。4.3 排序stable 排序 vs 单字段排序排序我一开始用的是 sorted(students, keylambda s: s[total])但这默认从小到大所以要加 reverseTrue。sorted_students sorted( students, keylambda s: s[total], reverseTrue )然后我就想总评相同的时候怎么排序题目没说。但 Python 的 sorted 是稳定排序也就是说如果总评相同原来的顺序会被保留。那原来的顺序是哪来的是我从 CSV 读进来的顺序。这个细节可能对老师来说无所谓但我觉得一个成绩统计程序按学号排一下更合理。所以我改成了双关键字排序sorted_students sorted( students, keylambda s: (s[total], s[sid]), reverseTrue )这段代码的缺点是reverseTrue 会把学号也变成降序。如果你想让总评降序、学号升序得这样写sorted_students sorted( students, keylambda s: (-s[total], s[sid]) )一个小技巧加负号代替 reverseTrue把排序方向控制到单字段级别。这个思路后来在很多场景都帮我避免了“反向全反”的尴尬。4.4 输出与写文件编码问题在 Windows 上特别痛控制台打印用中文一点问题没有。但往文件里写中文Windows 上默认编码可能是 GBK而 PyCharm 里默认又是 UTF-8两边不对上就会出现乱码或者 UnicodeEncodeError。最稳妥的方案是写文件的时候显式指定编码with open(result.txt, w, encodingutf-8) as f: f.write(\n.join(lines))但注意如果老师用的是 Windows 记事本打开你的 result.txtUTF-8 文件没有 BOM 头的话记事本老版本可能也会显示乱码。这种情况可以考虑 encodingutf-8-sig它会多写三个不可见字符。我自己后来在 Windows 上交付文本文件都会默认加 utf-8-sig市面上很多课程作业其实都不在意这个但你做了至少不会错。4.5 给代码加注释但别注解得像凑字数我第一次提交作业的时候注释写了满屏基本每行都加一句“把这一行读取进来”。后来学了点工程经验才明白注释要写的是“为什么”不是“是什么”。比如# 成绩相同情况下按学号升序保证输出顺序可复现 keylambda s: (-s[total], s[sid])这种注释才有价值。至于“读取一个学生记录”这种话看一眼代码就知道了注释纯粹是噪音。第一次作业的注释是非常加分的项因为它直接向老师传递一个信号这个人知道自己在写什么。5. 调试实录那些让我半夜崩溃的 bug作业写得快不代表 bug 少。我那次遇到的几个问题我挑典型的、每个新手大概率都会遇到的写出来。5.1 第一个 bug列表下标越界报错信息大概长这样IndexError: list index out of range。出在哪我在计算总评的时候访问了 scores[2]但有一行数据的第三列是空的。原因特别蠢CSV 文件里有一行末尾逗号后面是空格csv.reader 读完以后这一行的字段比正常行少了一个。解决方案是加一行数据清洗字段数不足的直接跳过或者补默认值if len(row) 5: continue这个处理看着丑但对应的是真实地从异常数据里保护程序。你也可以用 try-except 包住但在作业场景里一个 if 判断就够清楚了。5.2 第二个 bugsort 排序结果“看起来”不对劲排名出来后我发现有两个人的名次好像反了。查了半天发现一个学生的期末成绩是 89.5另一个是 89.49。我 sort 的时候用的是总评两个总评在两位小数上完全相同但实际原始分数不同。这个 bug 让我意识到一个事情如果 sort 的 key 只取总评那丢失精度是必然的。后来我把 key 改成原始加权未取整的值也就是不在 calculate_total 里 round而是在最终展示时用 format 控制两位小数输出。这个修改一石二鸟排序更精确输出也干净。def calculate_total(scores, weights(0.2, 0.3, 0.5)): return sum(float(s) * w for s, w in zip(scores, weights)) # 展示时四舍五入 print(f{student[total]:.2f}) # round 的逻辑交给 format5.3 第三个 bug反复打开文件导致句柄堆积我在调试的时候习惯每改一次都跑一遍程序结果发现越跑越慢。这个其实不算程序的 bug是我自己的问题我在终端开了一段循环跑脚本但每次打开 result.txt 都没关句柄越来越多。Python 进程结束时会自动释放但我是在交互式环境里反复执行代码段调试的所以积累起来了。用 with open 就是为了避免这种问题。当时 PyCharm 里报了个 ResourceWarning我一查才知道是这个原因。从那之后写文件必用 with板上钉钉。5.4 调试技巧print 大法强无敌刚学编程的时候正经 debugger 我根本不会用。我做的就是在每个关键节点打一行 print把中间结果打出来看。比如print(read rows:, rows[:3]) print(calculated:, students[:3]) print(sorted:, sorted_students[:3])这一串 print 打出来之后我就知道是数据读错了、计算错了还是排序错了定位问题基本不超过三分钟。等代码能跑通了再把这些临时 print 删掉或者改成日志不会耽误多少事。6. 常见问题速查与避坑清单我整理了一份当时遇到的问题清单也包含一些后来帮学弟学妹改作业时遇到的问题。这个表格可以直接当成自查表用。现象常见原因解决思路文件找不到工作目录不对、路径写错用 Path(file).parent 构造绝对路径中文乱码文件编码不一致读写路径统一用 encodingutf-8 或 utf-8-sig数字格式不对读进来是字符串没转 float显式类型转换加 float()排序方向反了reverseTrue 作用范围太大用负号字段控制单字段方向输出带有长尾小数浮点数精度问题展示时用 format 控制位数list index out of range数据行字段数不一致读取后检查字段长度程序跑完没反应函数定义了没调用检查主流程是否漏了调用步骤控制台能跑双击 .py 闪退没有 input 或窗口直接关闭末尾加 input() 暂停或打包时注意代码缩进不一致报 IndentationError混用 Tab 和空格统一用 4 空格缩进6.1 自查清单交作业前五分钟照着过一遍我后来养成了一个习惯所有作业提交前五分钟必须按照这个清单过一遍重新读一遍题目确认每一句要求都有对应的功能实现用干净环境跑一次程序确认不依赖我自己电脑的某个自定义环境删除调试用的 print 语句检查文件名、提交格式、命名规范是否与要求一致代码文件加一个简单的文件头注释说明学号和姓名确认打开输出文件验证内容而不是只看控制台结果这个清单很简单但每一条都是我用“扣分”换来的。尤其是第二点很多人的代码在自己电脑跑得好好的换个目录就崩就是因为写了绝对路径或者依赖了某个自定义模块。6.2 不要抄作业但可以“拆作业”说句实在话第一次写作业看到同学已经弄完了那种焦虑感谁经历过谁知道。但直接复制别人的代码你省下的只是时间损失的是这个完整的试错过程。我当时是找了一份学长写的作业不是抄而是“阅读源码”边读边问他为什么这么写。等我自己动手写完我发现他的结构还没我清晰呢。看清代码的逻辑权比拥有一份能跑通的代码重要得多。第一次作业最大的收获就是这个它会让你从“看代码都晕”变成“能拆开一段代码讲清楚它是干嘛的”。7. 一些我后来才想明白的经验回头看第一次作业它的价值不在于那个成绩统计程序本身而在于它逼着我完成了几个认知升级第一是一段程序应该拆成多个函数而不是从头写到底。一开始我整个程序就是一段流程式的代码后来一个地方出错就得从头扫到尾。拆成函数之后每个小方块可以单独测试定位问题快了不止一倍。第二是输入数据的质量会直接决定程序的复杂度。当时的作业数据几乎完美但让我提前想了一遍“脏数据”的问题这个习惯在后来处理真实项目时直接救了我的命。真实的数据永远是乱糟糟的缺字段的、格式错的、交叉重复的什么都有。在程序入口就把数据清洗做好后面逻辑部分就能省太多心。第三就是别怕改代码。我第一版写得很糟糕函数命名全是 temp1、temp2 这种后来重新理了一遍虽然代码长了点但每个函数都能说得出来它干嘛。重构不是老师要求的但做完之后我对这份代码的信心完全不一样了。最后再分享一个小技巧把所有作业放进一个以课程命名的文件夹里每份作业建一个子目录命名格式用“日期-课程-作业名”。我当时随便乱放结果学期末整理的时候发现自己连“第一作业.py”和“作业1.final.py”这种文件名都有三个版本。从第一次作业开始建立清晰的目录习惯后面整个学期都会感谢自己。
返回列表