1. 为什么“第二次作业”才是真正开始学东西的时候
先说说背景。这次作业是我在一门开发实践课上拿到的第二个任务,主题是“做一个能用的个人记账小工具”。第一周大家还在熟悉环境、跑通“Hello World”,第二周直接上这种要完整设计、能跑、能展示的小项目,说实话当时拿到题目的瞬间我是有点慌的。但你真做下来会发现,第一份作业是让你熟悉工具链,第二份作业才是真正逼你开始思考“一个程序到底是怎么从零长出来的”。
这篇文章我会把这次作业从需求分析、技术选型、代码实现到踩坑排查的全过程拆开来讲。核心关键词就三个:需求拆解、技术选型、问题排查。适合正在学编程、准备做课程设计或者想完整走一遍小项目流程的朋友参考。不管你是刚学Python还是已经会点前端,这篇内容都能帮你少走一些弯路。
这次作业的核心要求是这样的:做一个命令行版的个人记账工具,支持收入、支出记录的增删改查,能按类别统计,数据要存到本地文件里,重启后数据不丢。不要求界面,但要求和交互必须清晰。看着不复杂,但真正动手之后你就会发现,越是这种“看起来不复杂”的需求,越考验你对数据格式、存储方案、异常处理这些基础问题的把控能力。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整体设计与需求拆解
2.1 先把需求翻译成“程序听得懂的话”
拿到需求第一件事不是写代码,而是把中文需求逐条拆掉。这是我从这次作业里最大的收获。
原始需求里“支持收入、支出记录的增删改查”——增删改查是四个操作,也就是Create、Read、Update、Delete,合起来就是CRUD。那每一条记录应该长什么样?至少要有:金额、类别、时间、备注。这四样缺一不可。
“能按类别统计”——意味着数据需要有一个固定的类别字段,而且类别不能乱填。我当时定义了一个类别表,支出包含餐饮、交通、购物、其他,收入包含工资、兼职、其他。这里有个细节:如果用户输入了不在列表里的类别,程序要给出提示而不是静默接受,否则统计出来的数据就是脏的。
数据存到本地文件,重启不丢——这意味着我需要在内存里维护一个“当前会话的数据列表”,每次操作后同步写盘。选项有三:JSON文件、CSV文件、SQLite。当时我选了JSON,原因后面说。
2.2 为什么我选了JSON而不是SQLite
很多教程一上来就推荐SQLite,但在这个作业场景下,JSON反而是更合理的选择。原因有三点:
第一,JSON格式直接对应Python的字典和列表,读写都只需要json模块的load和dump,不需要额外的SQL语法知识。第二,这个工具的数据量级就是个人记账,一天几十条顶天了,JSON文件读进内存也就几十KB,完全没有性能压力。第三,后面如果要做数据可视化,JSON可以直接被前端读取,不用再做一次格式转换。
但JSON也有一个坑:写入文件时如果程序中途崩了,文件会损坏,之前的数据全丢。所以我当时设计了一个安全写入方案:先写到一个临时文件,写成功后再替换正式文件。这个技巧在作业里看起来不起眼,但放到真实项目里就是防止数据丢失的底线操作。
2.3 功能模块怎么切分
一个再小的项目也要有清晰的模块边界。我当时把程序分成了三个模块:
第一个是数据层,负责从JSON文件加载数据、把当前数据写回文件、记账记录的增删改查。第二个是业务层,负责处理统计逻辑,比如“当前月支出总额”“按类别汇总”。第三个是交互层,负责打印菜单、读取用户输入、调用业务层并展示结果。
这样分层的好处非常明显:如果后面想加一个“导入Excel”的功能,只需要改数据层;想加一个“图表展示”,只需要改业务层和交互层。每层之间通过函数接口通信,互不干扰。这对于一个两周后还要扩展功能的作业来说,就是给自己留后路。
3. 核心功能实现:从代码到设计思路
3.1 数据结构的定义
我定义每条记账记录为字典,字段如下:
python复制{
"id": 1,
"type": "expense", # 收入是 income,支出是 expense
"amount": 23.5,
"category": "餐饮",
"remark": "午饭",
"timestamp": "2025-06-11 12:30:00"
}
这里有几个细节值得说明。
id字段不直接用列表索引,因为后续做删除操作时,如果用列表索引删除,后面的记录索引会往前移,用户在用命令“删除第3条”时就会删错。用独立id以后,删除记录时按id匹配,其余记录完全不受影响。
timestamp用字符串而不是datetime对象,方便直接存JSON,也方便打印时不格式化。排序时用字符串比较其实也能达到目的,因为ISO格式的时间字符串本身就支持按字典序比较。
3.2 核心函数的实现思路
增删改查四个函数看起来简单,但每个都有“坑点”。
增加记录的难点在数据校验。金额字段必须转成float,如果用户输入“abc”,程序要捕获异常并重新询问,而不是直接崩溃。类别必须出现在合法类别列表里,否则拒绝写入。备注允许为空,这是有意的设计,因为有些记录确实不需要备注。
删除记录的难点在“查无此id”的情况。我当时设计的是如果用户输入的id不存在,打印“该记录不存在”,返回False,由主循环决定是否重新显示菜单。这里不需要报错,因为它不是异常,是正常的用户操作路径。
修改记录我是这样处理的:用户先输入要修改的记录id,然后程序显示当前记录内容,再要求用户逐项输入新的金额、类别、备注,留空则保持原值不动。这个交互设计比“告诉你第几个字段然后改”友好得多,也更符合人的直觉。
查询记录分为“列出全部”和“按条件筛选”。按条件筛选我做了三个维度:只查支出、只查收入、按类别查。这三个条件可以组合,也可以单独使用,实现上就是逐条判断、依次过滤。
3.3 统计功能的算法拆解
“按类别统计”这一步的核心逻辑就是做分组聚合。Python实现这种逻辑最简单的方式就是用字典:
python复制def summarize_by_category(records, target_type=None):
summary = {}
for record in records:
if target_type and record["type"] != target_type:
continue
category = record["category"]
summary[category] = summary.get(category, 0) + record["amount"]
return summary
这段代码的巧妙之处在summary.get(category, 0)——如果类别键不存在就返回0,加当前金额后再写回。这样避免了判断“键在不在字典里”的繁琐操作,一个get调用全搞定。
另一个统计是“区间汇总”,比如算本月支出或者2025年1月的总收入。这个的核心是解析时间字符串,然后比较月份。我用了strptime把字符串转成datetime,然后取year和month比较。这里有个很隐蔽的坑:用户在不同时区、不同系统下录入的时间格式可能带时区偏移,导致月份比较出错,但作业场景下统一格式就可以规避。
3.4 菜单交互的设计:让小白也能直接上手
交互层我做了两层设计。第一层是主菜单,显示六个选项:添加记录、查看记录、修改记录、删除记录、统计报表、退出。第二层是根据用户输入再进入子流程。
这里有一个我用过很多次的经验:不要直接让用户输入数字选择。因为数字对用户的认知负担很高——“5、统计报表”和“4、删除记录”之间,用户必须先看菜单再数编号。更好的方式是用中文首字母或简短的拼音命令。我当时用的是:a=add,l=list,u=update,d=delete,s=summary,q=quit。
这个设计的好处是:即使不用看菜单,用户也能靠直觉记住命令。你可能会说这不是增加记忆负担吗?恰恰相反,用户只需要记住一个字母,比记住一个数字加功能名要快得多。当然,菜单还是要打出来的,但不是必须每次盯着看。
3.5 安全写入:防止数据文件损坏的兜底方案
这部分算是本次作业的进阶亮点。直接往原文件里写数据的风险是:如果写入过程中程序被Ctrl+C中断,文件写入了一半,下次load这个JSON文件就会抛异常,用户之前所有的记账数据全没了。
我的方案是分三步写:
python复制import os
import json
import tempfile
def safe_save(data, file_path):
dir_path = os.path.dirname(file_path)
fd, tmp_path = tempfile.mkstemp(dir=dir_path, suffix=".tmp")
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
os.replace(tmp_path, file_path)
先用tempfile.mkstemp在同一个目录下创建临时文件,把数据写进临时文件,最后用os.replace原子替换原文件。os.replace在Linux和Windows上的行为是一致的:如果目标文件存在,替换操作是原子的,不会出现写一半的状况。
这里要补充一句:tempfile.mkstemp创建的文件权限是600,也就是只有当前用户能读写,这是好事。但要注意,如果你的程序后续要交给别的用户运行,文件的权限可能需要调整,否则对方会报“Permission denied”。
4. 实操过程记录:一步步做出这个工具
4.1 环境准备与项目结构
我的开发环境是macOS + Python 3.11,代码编辑器用的VS Code。项目结构很简单:
code复制expense_tracker/
├── data.json # 数据文件,初始为空列表
├── main.py # 程序入口
├── storage.py # 数据层:读写JSON
├── services.py # 业务层:统计逻辑
└── cli.py # 交互层:菜单和输入解析
四个文件的分工前面说过,这里就不重复了。重点是main.py只做一件事:初始化数据、进入交互循环。它不包含任何业务代码,这样将来如果要把整个工具改成网页版,只需要替换交互层和调用方式,数据层和业务层直接复用。
4.2 逐步编码:几个关键时刻的决策
我先写的是storage.py,因为它是其他模块的基础。storage.py里的load_data函数要处理的第一个问题是:文件不存在怎么办?这里我选择了返回空列表,而不是抛出异常。因为程序第一次运行时本来就该是空数据。如果你抛异常,用户还没开始记账就先看到错误提示,体验极差。
然后是services.py。写统计函数之前,我先定义了合法的类别列表,这个列表放在模块顶部作为常量。后面所有校验逻辑都引用这个常量。这样做的好处是,如果后面想加一个“医疗”类别,只需要改这一处,不用全代码搜索。
cli.py是最繁琐的,因为它要处理各种输入。我的经验是:每个输入函数单独写一个,不要在一个大函数里堆逻辑。比如input_amount、input_category、input_remark三个函数各管一件事。这样代码的每一个函数都能单独测试,出了bug也容易定位。
4.3 测试过程:手动测试和边界测试
写完所有代码后,我做了三轮测试。
第一轮是正常路径测试:添加一条收入、两条支出,查看列表,修改第一条支出,删除第二条支出,最后看统计报表。这轮测试主要验证主流程没问题。
第二轮是异常输入测试:输入金额“abc”、输入不存在的类别、删除不存在的id、修改时输入空值。这轮测试的目的在于确认程序不会在任何异常输入下崩溃——这是作业评价的重要维度之一。
第三轮是数据持久化测试:添加记录后退出程序,重新运行,确认记录还在;同时检查data.json文件的内容是否格式正确。这轮测试是我认为最重要的一轮,因为很多人在作业演示的时候,都是在当前进程里看着数据“在”,但重启之后数据全没了,现场演示就会翻车。
4.4 一个让我改了最久的Bug:输入的残留换行
我花了一个小时排查一个怪问题。操作是这样的:在主菜单输入“a”进入添加流程,输入金额后按回车,程序报错“无法识别类别”。
排查之后发现,问题的根源出在input()函数的换行处理上。当用户输入金额“23.5”,我用float()转换成功了,但如果用户多打了一个空格或者不小心输入了全角字符,float()就会抛出ValueError。这个不是Python的bug,是输入清洗没有做好。
解决办法是给所有输入函数加一个clean_input包装:
python复制def clean_input(prompt=""):
raw = input(prompt).strip()
return raw
strip()会把首尾的空格、换行、Tab都去掉。有了这一层封装,后面所有输入都走这个函数。这个教训让我记住了一件事:永远不要信任用户的输入,哪怕它是你自己在终端里输入的。
5. 问题排查与避坑技巧实录
5.1 问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 启动时JSON加载失败 | 数据文件被写坏或手动改错 | 用safe_save原子写入,提供备份文件 |
| 添加记录后退出程序数据丢失 | 只改了内存没有写文件 | 每次添加后立即调用save_data |
| 金额报错“无法转换为float” | 输入了非数字或全角符号 | 输入清洗,统一英文数字格式 |
| 中文乱码 | open没指定encoding | 读写文件都加上encoding="utf-8" |
| 删除记录后列表乱序 | 使用列表索引删除 | 改用id字段匹配删除 |
5.2 防数据丢失要“每次操作都落盘”
我见过很多人只做完增删改后不保存,最后退出的时候才save一次。如果中间程序因为某种原因退出——比如Ctrl+C、断电、断网——用户的全部操作就都白费了。
我的做法是:每次添加、修改、删除成功后,立即调用save_data。虽然性能上有微小损耗,但换来的是数据安全。对于个人记账工具来说,这个取舍值得做。
5.3 展示统计结果时,永远不要依赖打印顺序
Python 3.7以后,dict保持插入顺序。但如果你想让统计结果按金额从大到小排列,必须显式排序:
python复制def format_summary(summary):
sorted_items = sorted(summary.items(), key=lambda x: x[1], reverse=True)
for category, total in sorted_items:
print(f"{category}: {total:.2f}元")
sorted的key参数指定用金额排序,reverse=True表示从大到小。这个小细节能让你的统计表好读很多,尤其是类别多了以后,不排序的话用户要在一堆数字里自己找最高的那项。
6. 复盘:这次作业到底让我学会了什么
很多人做第二次作业的标准是“能跑起来就行”,但如果你愿意多花三个小时去完善它,收获会完全不一样。
我个人感受最深的有三点:
第一,分层设计不是面试时的概念,而是真真切切能让你少加班的东西。当我写统计功能时完全不用管数据从哪来,当我改交互方式时完全不用动统计逻辑——这种“改动局部不影响全局”的爽感,只有被烂结构坑过才能体会到。
第二,异常处理的价值不在于“让别人看起来专业”,而在于“用户怎么用都不慌”。我加了几个try-except之后,自己再测试时那种“故意输入错误看它出什么提示”的体验,比写功能本身还有意思。
第三,代码提交前一定要做“从零开始”的完整测试。很多人只测试自己开发过程中的数据,但忘了把data.json删掉、从空数据开始跑一遍完整流程。这一步多花五分钟,能拦住至少一半的演示翻车。
这个工具后续如果要扩展,我建议加上这三个方向:一是数据导出成Excel,方便存档;二是加一个月度趋势统计函数,看看每个月的支出变化;三是做一个简单的图形界面,用Tkinter就够,把统计结果画成柱状图。每一步都不难,但每一步都能让这个“第二次作业”变成一个真正属于你的小作品。
