先说个实际感受整理这套 py03 学习笔记的过程比我预想的更有收获。py03 并不是什么高深莫测的框架它就是 Python 学习路径里一次编号为 03 的综合实战练手项目我拿着它做了一个命令行版的学生成绩管理工具。做完之后我最大的体会是——很多知识点单独学都懂但凑在一起写一个能真正运行、能反复使用的小工具才是把书本内容变成自己能力的分水岭。这篇笔记适合正在学 Python 基础语法、想找一个小而完整的项目练手的人也适合那些已经能看懂教程里单段代码、但一动手就不知道怎么组织文件的初学者。我会把整个项目的设计思路、代码实现、踩过的坑和排查过程都写出来代码可以直接复制去跑。1. 先把 py03 是什么讲清楚1.1 别被编号吓到它其实就是一次综合实战很多自学者看到“py03”这种名字会以为是一个特定的技术名词或某个开源库实际上它更像是一套练习任务里的第三个关卡。我自己通常把 Python 学习分成这么几个编号任务py01 是基础语法练习py02 是函数与模块小练习到了 py03就该把之前学的东西串起来写一个带交互、带数据存储、带业务逻辑的完整小工具。py03 这个定位非常关键。它不是让你炫技去用多高端的框架而是逼着你回答一个问题如果不用现成的图形界面框架只靠命令行你能不能写一个“真正能用”的东西我选择学生成绩管理是因为这个场景足够典型需要对一组结构化数据进行增删改查需要把数据保存到磁盘需要在下次启动时重新加载还需要处理用户输入的各种意外情况。这几个需求几乎涵盖了日常开发里 80% 的基础功。做完这个项目之后你会发现自己对函数之间的调用关系、数据在内存里的组织方式、文件读写时编码格式的影响都有了比之前深刻得多的理解。它不解决具体某个业务难题但它是你后面去理解爬虫、Web 后端、数据处理时的地基。1.2 适合谁看以及这个项目能帮你补齐哪些短板我认为 py03 最适合两类读者。第一类是刚学完 Python 基础语法正处在“看啥都懂、动手就蒙”阶段的人你需要一个足够小、但五脏俱全的项目来打破“只能照着教程敲”的僵局。第二类是像我一样做过零散练习但从来没把一个程序从零写到结束的人你缺的不是某个知识点而是完整的工程思维。这个项目能补齐的短板也很明确。第一模块化思维。当你把数据层、业务逻辑、交互循环拆到不同函数甚至不同文件时你才会理解什么叫“各司其职”。第二对数据生命周期的掌控。从程序启动时的加载到运行中的修改再到退出前的保存这一个闭环能让你彻底明白内存数据和磁盘文件的关系。第三排错能力。一个几十行的交互程序里可能出现编码错误、类型错误、路径错误、数据覆盖问题你每解决一个就等于往经验库里存了一条宝贵记录。提示如果你已经能很熟练地写函数和类可以跳过 1.1 直接看后面的架构与实现部分。2. 项目整体设计与任务拆解2.1 先别写代码把功能清单列出来我见过太多初学者拿到项目就开始硬写写到一半发现功能之间互相纠缠最后只能推翻重来。正确做法是先花半个小时把需求想清楚。我的 py03 成绩管理工具最终功能清单是这样的添加一条学生成绩记录字段包括学号、姓名、课程、分数。删除指定学号的记录。修改指定学号的分数。按课程筛选出所有记录并按分数从高到低排序。统计某门课程的平均分、最高分、最低分和总人数。展示全部记录的表格。程序退出后数据不丢失下次启动能自动恢复。别小看这张清单。它每一条都在逼你回答一个技术问题。比如“按课程筛选并排序”你就要考虑数据用什么结构存放是列表还是字典排序时用 sorted 的 key 参数怎么写“修改分数”要求你不能只修改内存还要把改动同步回文件“退出后不丢失”意味着你要设计一个明确的数据保存时机。我把这些需求按优先级排了个序最核心的是增删改查然后是统计和筛选最后才是表格展示和异常处理。如果时间紧张先做前两个也能跑起来再逐步完善。这个思路也直接决定了我后面的代码结构核心功能之间的边界要尽量清晰不能把展示逻辑和业务逻辑写成一团。2.2 模块划分的三种方案我最终选了哪套对于这种规模的项目模块划分通常有三种做法。第一种是单文件硬写所有函数全堆在 main.py 里。优点是省事缺点是一旦要加功能文件立马变得臃肿。第二种是按照功能模块拆成多个文件比如 users.py、stats.py、storage.py每个文件只负责一类事情。第三种是更进一步引入类来封装数据和行为。我最后选择的是“单项目多文件 少量 dataclass”的方案。因为 py03 的核心目标是练基本功我不想把太多精力花在类继承和抽象上但也不想退化成单文件堆代码。实际拆分后是这样的main.py启动入口和主交互循环。storage.py负责从 JSON 文件加载数据、把数据写回文件。models.py定义 Student 数据结构和相关校验逻辑。services.py放增删改查、筛选统计的具体业务逻辑。这样的好处是当你想把命令行交互换成 Web 接口时只需要改 main.pystorage 和 services 基本不用动。我在后面的实操部分会完整展示每个文件怎么写。2.3 存储方案选型JSON 为什么够用一开始我也纠结过数据到底存哪里是存 JSON 文件csv 文件还是直接上 sqlite我最终选了 JSON原因有三个。第一py03 的数据量很小几十条到几百条记录完全轮不到数据库出场文件存储的读写速度已经快到感知不到。第二JSON 和 Python 的字典、列表结构天然接近序列化和反序列化几乎零成本能让初学者把注意力放在业务逻辑上。第三JSON 文件是可读的出问题了你还能用文本编辑器直接查看和修改排错难度明显更低。csv 的问题在于它没有类型信息分数存进去再读出来是字符串需要手动转换。sqlite 很强但引入 SQL 概念会给初学者增加额外的认知负担和 py03 的定位不符。当然如果你的 py03 项目定位是“模拟真实项目”那选 sqlite 也完全可以只是要接受代码量会大不少。提示选 JSON 不代表忽略数据安全性。比喻一下JSON 文件就像一张纸质账本程序每次修改数据相当于在账本上重写一遍全部内容。数据量小时没问题数据量大了以后再考虑 sqlite 这种更专业的方案不迟。3. 核心细节解析与实操要点3.1 数据模型用 dataclass而不是普通字典很多初学版本的成绩管理程序会把学生记录直接写成字典比如{name: 张三, score: 88}。这种写法不是不行但当代码里多处出现student[score]这种访问方式时一旦键名拼写错误错误要到运行时才暴露。我在 py03 里使用了 dataclass让数据结构变得清晰且带类型提示。from dataclasses import dataclass dataclass class Student: student_id: str name: str course: str score: float使用 dataclass 的核心好处有三个。第一字段名是静态的IDE 能自动补全拼写错误在写代码时就能发现。第二面向对象的方式更贴近真实业务一个 Student 实例天然代表一条记录语义明确。第三配合asdict()函数转回字典做 JSON 序列化极其方便这比手动构造{score: stu.score}安全得多。需要注意的是如果你在数据里加了一个字段比如想记录“考试日期”那么 dataclass 定义、load 函数、表格打印函数都要同步调整。初学者常见的错误是只改一个地方导致运行时提示TypeError: __init__() got an unexpected keyword argument。3.2 JSON 读写中的四个关键细节如果你以前写过 JSON 文件读写大概率遇到过这几个问题中文变成一堆\uXXXX、文件路径找不到、数据读出来没有类型、写入时覆盖了原有内容。我在 py03 里逐个处理掉了下面是核心的 storage.py 写法import json from pathlib import Path from models import Student DATA_FILE Path(students.json) def load_students() - list[Student]: if not DATA_FILE.exists(): return [] with open(DATA_FILE, r, encodingutf-8) as f: raw_data json.load(f) return [Student(**item) for item in raw_data] def save_students(students: list[Student]) - None: raw_data [student.__dict__ for student in students] with open(DATA_FILE, w, encodingutf-8) as f: json.dump(raw_data, f, ensure_asciiFalse, indent2)这里有几个细节要重点讲。第一个是encodingutf-8少了这个参数在 Windows 平台上默认编码可能是 gbk遇到中文直接崩溃这是新手必踩的第一个坑。第二个是ensure_asciiFalse不加的话JSON 文件里会全是\u5f20\u4e09虽然能读回来但没法用文本编辑器排查问题。第三个是路径处理我用了pathlib.Path比字符串拼接更安全文件和脚本放在同一目录时直接Path(students.json)就能正确定位。第四个是保存逻辑每次保存都是把整个列表序列化后整体写入所以要保证调用 save 时传入的是最新的完整列表而不是某条记录的副本。3.3 交互层最容易翻车的地方输入循环和退出条件命令行交互程序的核心循环是展示菜单接受输入执行动作回到展示菜单。这个循环设计不好的话会出现几种非常挫败的体验程序在异常输入后直接崩溃、退出时数据没保存、按错一个键就无法挽回。我建议把主循环写成“状态机”形式虽然听起来高级其实逻辑很简单def main(): students load_students() while True: show_menu() choice input(请输入操作编号).strip() if choice 1: add_student(students) elif choice 2: remove_student(students) elif choice 3: modify_student(students) elif choice 4: query_by_course(students) elif choice 5: show_stats(students) elif choice 6: list_students(students) elif choice 0: save_students(students) print(数据已保存欢迎下次使用。) break else: print(无效输入请重新输入。)这段代码看起来简单但有几个细节值得琢磨。第一input()返回的字符串默认会带换行和空格我用了.strip()防止用户多敲空格导致匹配失败。第二保存动作只在“0”退出时触发而不是每次操作后都写盘这样既保证数据不丢又减少 IO 次数。第三break前必须先执行保存这是整个程序的关键闭环。很多人的程序写完了数据却一直丢就是因为把保存逻辑放在了循环之前或者漏掉了。注意如果你在 save 之前用return或者sys.exit()退出了循环数据就永远不会写回文件。这一点非常隐蔽。4. 实操过程与核心环节实现4.1 目录结构和常量设计我在动手写代码前先把目录定下来了。对 py03 这种项目不需要搞复杂的包结构但至少要让单文件也能跑通的代码变成拆开之后还能跑通。最终目录是这样的py03/ ├── main.py ├── models.py ├── storage.py ├── services.py └── students.json前三行是代码文件第四个文件是本项目所有数据的唯一来源。如果你把存储文件也放进代码目录在打包或复制项目时要注意一并带上如果想让数据文件不污染代码目录可以把存储路径设计成“缺省时自动创建”。我说下我的做法students.json放在项目根目录下和main.py同级这样最直观也方便文本编辑器直接打开检查。常量部分我建议集中在 storage.py 顶部不要散落各处。路径、字段名、默认排序方式都属于“可能变化的东西”集中定义后日后想改存储文件名只需动一个地方。这一点虽然简单但对培养工程习惯非常有帮助。4.2 数据层实现加载与保存要能互相配合数据层是整个项目的地基。加载和保存就像一对齿轮必须咬合紧密。我在 storage.py 里用Student(**item)的方式从 JSON 恢复对象前提是 JSON 里的字段名和 dataclass 字段名完全一致。如果你在保存时用了自定义的键名比如id而不是student_id那加载时就要自定义转换逻辑不要犯懒。另一个注意点是空文件的处理。第一次运行程序时students.json不存在load_students()会返回空列表。但如果用户手动把文件内容清空了json.load会抛JSONDecodeError。所以我加了一层保护如果文件不是合法 JSON就当作空数据处理同时打印一条提示而不是让程序直接崩溃。这个保护代码虽然只有几行但能让你在手工编辑文件出错的时候不至于一脸蒙。实现完数据层后我习惯立刻写几行测试比如先构造两条学生记录保存再重新加载看打印出来的对象是否一致。这种“最小闭环”测试非常划算它能在第一时间发现字段名不匹配、数据类型丢失之类的问题。4.3 业务层实现增删改查怎么写才不混乱业务层的核心是操作一个可变的学生列表。我把所有操作都设计成“传入列表直接修改列表”这样主循环里的students变量始终是同一份数据避免了“修改的是副本、保存时还是旧数据”的经典错误。添加记录的代码如下def add_student(students: list[Student]) - None: student_id input(请输入学号).strip() if any(s.student_id student_id for s in students): print(该学号已存在。) return name input(请输入姓名).strip() course input(请输入课程).strip() score float(input(请输入分数).strip()) students.append(Student(student_idstudent_id, namename, coursecourse, scorescore)) print(添加成功。)这段代码里有两个细节必须提醒。第一输入分数时不能直接信任用户的字符串float()转换失败会让程序崩溃更稳妥的做法是用try...except包住转换。第二学号唯一性检查用到了any()加生成器表达式这比手动写循环更符合 Python 风格也更容易阅读。删除和修改功能同样要先用学号定位。我的实现是找到匹配学号的下标删除或替换对应元素。这里最容易出错的是“删除不存在的学号”所以一定要先判断 index 是否存在否则pop()会抛 IndexError。统计功能的实现思路是先用列表推导式筛选出指定课程的所有分数再用内置函数计算。这三个内置函数min、max、sum在班里成绩统计的场景下非常好用。使用round(...,1)可以控制平均分的小数位避免出现无限长的小数尾巴。4.4 展示函数表格打印的经验之谈命令行程序里展示数据最朴素也最实用的方式就是打印表格。但直接用print(student)的话输出又丑又难读。我的做法是先算好每列的宽度再用固定宽度格式化字符串。def list_students(students: list[Student]) - None: if not students: print(暂无记录。) return headers [学号, 姓名, 课程, 分数] col_widths [10, 12, 10, 6] header_line | .join(h.ljust(w) for h, w in zip(headers, col_widths)) print(header_line) print(- * len(header_line)) for stu in students: row [ stu.student_id.ljust(10), stu.name.ljust(12), stu.course.ljust(10), f{stu.score:6.1f}.ljust(6), ] print( | .join(row))这个实现里有个小细节f{stu.score:6.1f}把分数格式化成保留一位小数的左对齐字符串同时保证了列宽。对于中文姓名ljust的宽度计算是按字符数而不是显示宽度所以中文对齐会略有偏差但在这个项目里完全够用。按课程筛选并排序的实现则体现了 Python 函数式风格的妙处def query_by_course(students: list[Student]) - None: course input(请输入课程名).strip() matched [s for s in students if s.course course] matched.sort(keylambda s: s.score, reverseTrue) ...筛选用了列表推导式排序用了 lambda 指定排序键reverseTrue表示从高到低。只要理解了这两个语法点其他任何“按 XX 排序”的需求都能照葫芦画瓢。5. 常见问题与排查技巧实录5.1 中文写入 JSON 变成\u转义怎么办这是一个概率极高的新手问题。写入文件后打开 students.json看到的不是“张三”而是\u5f20\u4e09。原因就是json.dump默认把非 ASCII 字符转成转义序列。解决办法非常简单给json.dump加上ensure_asciiFalse参数。我见过一些人为了规避这个问题硬是把数据改成拼音或英文完全没有必要。排查这类问题有个好习惯写完文件后用代码而不是文本编辑器判断结果。在 Python 里读回来打印一下repr()如果恢复出来的是中文说明数据链路没问题。文本显示乱码时先检查读取时是否指定了encodingutf-8。编码问题 90% 集中在“写时没指定编码”或“读时用错编码”这两处。5.2 程序一运行就崩溃的三种典型原因我在这个项目里遇到的崩溃归纳起来就三种。第一种是文件不存在却直接读报FileNotFoundError。解决办法是像 storage.py 里那样读取前用DATA_FILE.exists()做判断。第二种是 JSON 里数据结构和 dataclass 匹配不上报TypeError或者ValueError。比如你删除了 JSON 文件里的一条记录但那条记录还带有一个 dataclass 里没有的字段。第三种是分数转换失败用户在提示输入分数时敲了“abc”float()抛ValueError。我的建议是不要试图一次性解决所有崩溃而是先看报错的最后三行。追踪栈信息里通常已经明确告诉你是哪个文件、哪一行出问题。新手最容易犯的错是只盯着报错信息的第一行看结果越看越糊涂。真正的排查顺序应该是先看异常类型再看出错行号最后结合上下文判断数据状态。5.3 修改了数据却总感觉“没保存”的坑这是我在 py03 里踩得最深的一个坑。现象是程序运行中新增了记录退出后再启动记录还在但如果你中途用 CtrlC 强制终止程序新增的数据就全没了。原因是我们只在退出菜单的“0”分支里调用了save_students()而 CtrlC 触发的KeyboardInterrupt直接跳出了循环根本没有执行保存代码。这个问题有两个解决思路。思路一把保存时机改成“每次操作后立即保存”优点是数据不容易丢缺点是频繁写盘性能略差对 py03 这种小项目其实无所谓。思路二用try...finally确保退出前必定保存def main(): students load_students() try: while True: ... finally: save_students(students)这样无论如何退出都会执行保存。我最后采用了这个方案。请记住这个原则需要持久化的数据保存动作不能只放在某一个“预期正常退出”的路径上而是要放在无论如何都会执行的位置。5.4 常用排查技巧速查表现象检查位置解决方向中文全是\ustorage.py dump加ensure_asciiFalse文件不存在storage.py load先用 exists() 判断数据是旧版本save 调用时机确保保存的是最新列表用户输入非数字services.py用 float() 包一层 try字段对不上models.py 和 JSON保持字段名一致退出时丢数据main.py 循环用 finally 兜底保存这张表建议你贴在你的 py03 笔记本旁边。它不是万能药但能帮你把 80% 的报错快速定位。6. 个人实操体会与扩展方向做完整套 py03 后我最大的感受是小项目真正考验的不是某个语法点而是你能否在“数据怎么存”、“异常怎么处理”、“逻辑怎么拆”这些看似琐碎的选择里做出更合理、更面向改动安全的决定。我以前总想着代码写得越少越好现在反而意识到写清楚几个小函数、加几行异常保护看起来多了几行但运行时的稳定性和自己排查问题的效率都远远胜过毛糙的单文件版本。最后分享一个我一直在用的小习惯每次做完这种练手项目都给它写一个 README记录项目能做什么、怎么运行、文件结构是什么。表面上看是给未来的别人看实际上三个月后的自己真的会感激当时的这份记录。py03 的下一步扩展我已经想好了给成绩加上“日期”字段、支持按学生姓名模糊搜索、再把统计结果导出成 CSV。这些扩展每加一个都会逼着你去重新审视原来的架构是否扛得住这正是练手项目最大的价值所在。