Python命令行待办事项程序:从需求拆解到代码实现

发布时间:2026/10/11 13:54:26
Python命令行待办事项程序:从需求拆解到代码实现
这周五有个学弟在微信上跟我吐槽说他被课程里的第五次作业折磨到怀疑人生。作业内容其实很简单——写一个命令行待办事项管理程序支持添加任务、查看列表、标记完成、删除任务还要做到关闭程序后数据不丢。他连函数、字典、文件读写这些知识点都学完了可一动手就发现脑子里一片空白。我看了看他的代码瞬间明白了问题所在前四次作业都是照着例题改第五次作业第一次要求他从零到一完整地做一个小工具。这真的是很多人的第一道坎。这篇文章不打算给你一份可以直接抄的完整答案因为那是害你。我更想把第五次作业背后真正需要掌握的东西拆开讲透怎么把一句模糊的任务描述变成功能清单怎么设计数据结构怎么把代码写到别人能看懂、自己能维护的程度以及交作业前到底该测什么。这套思路不仅适用于编程课的阶段性项目放到日常工作中写脚本、做小工具也一样通用。下文会用 Python 来演示实现细节但拆需求、定结构、写函数这套方法论是语言无关的你换成 Java、Go、C 都一样成立。1. 为什么第五次作业是个分水岭1.1 前四次作业都在铺垫什么很多同学到第五次作业才突然觉得难度跳了其实不是难度跳了而是课程设计本身就在这儿埋了一个转折点。回顾一下典型的 Python 课程前四次作业第一次是变量与数据类型第二次是条件判断和循环第三次是函数的定义与调用第四次是列表、字典和文件读写。每一次的作业都很窄老师通常会给你一个模板你只需要在指定位置填空或者照着例题改改参数就能跑通。但第五次作业不一样。它把所有语法点全部揉到一个场景里逼迫你从理解代码切换到组织代码。这个过程非常像学车科目二练倒库、侧方、坡起都是单点动作到了科目三才要求你把这些动作用在一段完整的实际路线上。第五次作业就是编程课的科目三它考察的不是你背了多少语法而是你能不能把一个散落的知识点串成一条能跑通全流程的链路。我见过不少基础语法学得还不错的人到这一步突然熄火。常见表现是能看懂老师的示例代码但自己新建一个 py 文件之后不知道第一行该写什么或者把所有功能都堆在 main 和 while 循环里最后代码成了一锅粥改一个地方崩三个地方。这些都不是智力问题而是缺少先设计、再编码的习惯。第五次作业的意义就是逼你在低风险、低压力的环境下建立这个习惯。1.2 这次作业真正要考的东西如果一个老师布置了命令行待办事项程序他表面上是想让你练手文件读写和函数封装但批改的时候藏在评分细则里的往往还有四件事。第一是需求理解能力。你能否从添加、删除、标记完成、持久化这四句话里推导出程序至少要有交互菜单、数据结构、存储文件三个模块。第二是函数拆分能力。把增删改查四个操作拆成独立函数而不是一个大循环里四段 if 逻辑缠在一起。第三是边界情况处理。比如任务文件不存在怎么办、用户输入了空字符串怎么办、给了不存在的任务编号怎么办。第四是代码可读性。命名规不规范、有没有注释、函数职责是否清晰甚至 README 写不写都会影响老师对你代码的第一印象。换句话说前四次作业考的是代码能不能跑第五次作业考的是程序能不能用。能跑和能用之间差了很远——能用意味着别人不看你代码也能上手操作意味着程序在异常输入下不会直接崩掉意味着数据在重启之后还在。理解了这一层你就会明白为什么单把语法书背烂并不足以搞定这个作业真正拉分的是工程思维的雏形。2. 动手前先拆需求从一句话到一张功能表2.1 把作业要求翻译成用户故事拿到做一个命令行待办事项管理程序这句话先别急着打开编辑器。我在写任何代码前的第一步都是把需求翻译成用户故事也就是回答一个问题谁在用这个程序他会在哪个场景下做什么操作把这句话落在待办事项场景下就非常清晰了一个普通用户每天早上打开电脑运行程序看到一行行菜单。他要能输入1来添加今天要干的事输入2查看现在有哪些任务任务做完之后输入3把它标记为已完成如果输入错了或者任务取消可以输入4删掉它。最后用户希望今天添加的任务明天打开程序时还在这就引出了持久化存储。一个实用经验是把需求列成一张表格每一项功能旁边标注必须做还是加分项。课程作业给的时间有限先把主干功能跑通再去追求锦上添花。比如对于一个待办事项程序必须做的功能可以拆成下面这张表功能用户操作程序行为优先级添加任务输入任务标题保存任务并返回新编号必须查看任务选择查看列表以清晰格式打印全部任务必须标记完成输入任务编号修改该任务的完成状态必须删除任务输入任务编号从文件中移除该任务必须持久化存储关闭程序再打开任务保留上次的状态必须任务搜索输入关键词只显示匹配的任务加分按状态筛选只看未完成过滤列表加分这张表最大的价值是让你在写代码的过程中时刻知道自己有没有跑偏。我见过不少人大半夜还在折腾花哨的配色和动画主干功能却还没跑通这就是典型的没有把需求拆成清单导致的。2.2 数据怎么存结构怎么定需求拆清楚之后第二步是设计数据结构和存储方案。这一步极其关键因为它决定了后面所有代码的写法。对于一个待办任务最少需要四个字段任务编号 id、任务标题 title、完成状态 done、创建时间 created_at。创建时间不是必须的但加上它会让程序看起来更完整而且在展示列表时能提供很多便利。用 Python 的话一个任务就用字典表示。整个任务清单就是一个列表列表中每个元素是一个字典大概长这样tasks [ {id: 1, title: 写实验报告, done: False, created_at: 2025-06-20 09:30}, {id: 2, title: 买牛奶, done: True, created_at: 2025-06-20 10:00} ]至于存储格式我的建议是第一选择用 JSON 文件。原因有三个第一JSON 是纯文本老师拿到作业后用记事本就能看到存的是什么非常直观第二Python 标准库里有 json 模块读写只需要两行代码不需要装第三方依赖第三它的中文支持好指定 ensure_asciiFalse 就能正常显示汉字不会出现一堆 \uXXXX 转义符。不建议用 pickle 模块。虽然 pickle 可以直接把一个 Python 对象序列化到文件用起来更省事但它的文件是二进制格式不透明而且万一课程环境升级换 Python 版本旧文件可能读不出来。你自己写着爽老师批改时想看数据就得额外跑脚本属于给自己和阅卷人同时挖坑。存储文件的名称我建议固定叫 tasks.json并放在程序同一目录下。代码里用相对路径而不是绝对路径这样整个项目文件夹拷到任何电脑上都能跑。更稳妥的方式是定义一个常量import os TODO_FILE tasks.json def load_tasks(): if not os.path.exists(TODO_FILE): return [] with open(TODO_FILE, r, encodingutf-8) as f: return json.load(f) def save_tasks(tasks): with open(TODO_FILE, w, encodingutf-8) as f: json.dump(tasks, f, ensure_asciiFalse, indent2)load_tasks 里先判断文件是否存在这个细节非常关键我第一次写的时候偷懒没处理结果程序一启动就抛 FileNotFoundError后面又花了不少时间排查。这其实就是边界处理意识老师评分的时候特别爱看这类地方有没有考虑周全。3. 核心功能实现一个完整的待办事项程序3.1 数据层文件读写与 JSON 持久化前面给出的 load_tasks 和 save_tasks 就是整个程序的数据层它们是所有功能的地基。load_tasks 负责在程序启动时把磁盘上的数据加载到内存save_tasks 负责在每一次数据变更后立即写回文件。这两个函数虽然短但有几个细节值得展开。第一save_tasks 里指定 indent2会让 JSON 文件在编辑器中呈现清晰的缩进结构方便你事后手动查看。第二ensure_asciiFalse 必须带上否则中文任务标题会全部变成 \u5b66\u4e60 这种转义序列可读性直接归零。第三我建议每次都采用先写临时文件、再替换或者至少是直接覆盖写入因为对于课程作业这个规模的数据量性能不用考虑简单可靠才是第一位。补充一个实操心得在开发调试阶段每写完一个函数就手动打开一次 tasks.json 看看内容。这个习惯能帮你尽早发现保存成功但格式不对或数据被意外覆盖的问题。我见过同学把 save_tasks 写在循环外面结果每加一个任务要等到退出程序才全部保存中途断电就全没了。所以记住一个原则任何一次数据变更紧随其后就调用 save_tasks。3.2 业务层每个操作封装成一个函数接下来是核心的业务逻辑。我的建议是每个操作对应一个函数函数只负责一件事这样主程序会非常清爽。添加任务的实现可以这样写import time def next_id(tasks): if not tasks: return 1 return max(task[id] for task in tasks) 1 def add_task(tasks, title): task { id: next_id(tasks), title: title, done: False, created_at: time.strftime(%Y-%m-%d %H:%M:%S) } tasks.append(task) save_tasks(tasks) return task这里有个很容易踩的坑新任务的编号不能简单用 len(tasks) 1。如果你先添加了 3 个任务再把第 2 个删掉此时列表长度为 2用 len 计算出的新编号还是 3但已经有一个 id 为 3 的任务了于是出现重复 id。正确做法是用当前列表中的最大 id 加 1也就是上面的 next_id 函数。这两者的差异看似微小但一旦任务超过十几个重复 id 就会引发标记错任务、删除错任务的连锁问题。查看列表的展示也建议单独抽一个函数。展示不仅仅是打印还要注意格式。我的习惯是给每一项加上序号状态标识方便用户阅读def list_tasks(tasks): if not tasks: print(当前没有任务。) return for task in tasks: status [已完成] if task[done] else [未完成] print(f{task[id]:3} {status} {task[title]} (创建于 {task[created_at]}))标记完成和删除任务都要处理编号不存在的情况def complete_task(tasks, task_id): for task in tasks: if task[id] task_id: task[done] True save_tasks(tasks) return True print(没有找到这个编号的任务。) return False def delete_task(tasks, task_id): for task in tasks: if task[id] task_id: tasks.remove(task) save_tasks(tasks) return True print(没有找到这个编号的任务。) return FalseDelete 这里再提醒一点在遍历列表的同时修改列表是危险的比如 for task in tasks 里执行 tasks.remove(task)Python 会跳过下一个元素。上面这个写法是先找到目标再用 remove 删除属于比较安全的模式。如果要按条件批量删除建议生成一个新列表再整体替换。3.3 交互层主循环与输入解析数据层和业务层都就绪后最后拼上交互层。命令行程序的主流交互方式是打印菜单、等待用户输入、根据输入分发到对应函数。一个稳定的框架是 while True 循环加 if/elif 分发def main(): tasks load_tasks() while True: print(\n待办事项管理) print(1. 添加任务) print(2. 查看任务) print(3. 标记完成) print(4. 删除任务) print(5. 退出程序) choice input(请输入序号).strip() if choice 1: title input(请输入任务标题).strip() if not title: print(任务标题不能为空。) continue add_task(tasks, title) print(添加成功。) elif choice 2: list_tasks(tasks) elif choice 3: try: task_id int(input(请输入任务编号)) complete_task(tasks, task_id) except ValueError: print(编号必须是数字。) elif choice 4: try: task_id int(input(请输入任务编号)) delete_task(tasks, task_id) except ValueError: print(编号必须是数字。) elif choice 5: print(已保存再见。) break else: print(无效输入请重新选择。) if __name__ __main__: main()这段看似无聊的交互代码里藏着三个常见的翻车点。第一是 .strip()用户输入时随手敲个空格是常有的事不处理的话 1 和 1 不一致程序会走进 else 分支报无效输入。第二是 int() 转换input 返回的永远是字符串直接拿 abc 转 int 会抛 ValueError所以要套 try/except。第三是姗姗来迟的退出指令我记得第一次写的时候忘了在 elif 里放 break结果程序永远退不出去只能 CtrlC 强杀数据还停留在内存里没写盘。个中滋味写一次你就懂。4. 测试那些事我踩过的坑你先别踩4.1 四个高频 Bug 与排查思路写测试是最容易被作业党忽略的环节但恰恰是它最能拉开分数差距。我先把自己反复踩过的四个经典坑列出来每个都附带排查思路如果你想体验一下面向错误编程的酸爽可以故意不处理再跑一遍。第一个坑是文件不存在导致崩溃。程序第一次运行时 tasks.json 还不存在load_tasks 直接 json.load 就会抛 FileNotFoundError。我在前面已经给出了 os.path.exists 的判断方案。排查思路也很简单把项目目录里的 tasks.json 删掉再启动程序看它还能不能正常进入菜单。第二个坑是空文件导致 json.JSONDecodeError。当你第一次运行程序、添加任务后退出tasks.json 里只有内容但如果你偷偷用文本编辑器把文件内容清空成 0 字节再启动程序json.load 读不进来就会崩。更稳妥的 load_tasks 应该在 try/except 里处理返回空列表而不是在异常时让程序退出。这个处理对课程作业可能加分不多但它代表了一种防御性编程意识。第三个坑是删除任务时的列表迭代问题。如果你图省事写成了下面的形式for task in tasks: if task[id] task_id: tasks.remove(task)在列表较短时看着没问题但一旦被删除的任务后面还有元素remove 之后循环索引会跳过一个导致后面的任务被意外跳过。排查这个问题的典型特征是程序时报错且删一个任务后列表结构混乱。修复方案是删除后立刻 return或者构造新列表。第四个坑是 int 类型与字符串类型的误判。input 返回的是 str很多人拿它和 int 比较或者直接想把它当列表下标用。排查特征就是输入 1 之后毫无反应或 TypeError。好的习惯是立刻做类型转换并配 try/except 捕获 ValueError。我额外提一个中文环境特有的坑在 Windows 的控制台里print 中文没问题但打开 tasks.json 时如果不指定 encodingutf-8写入的文件用记事本打开可能是乱码或者再次读取时报 UnicodeDecodeError。这个坑非常隐蔽因为有的电脑默认编码可能是 GBK而你的代码里写的是 UTF-8。统一在 open 时显式指定 encoding 是最靠谱的方案。4.2 一张自测清单交作业前跑一遍下面是针对第五次作业这种规模的项目我总结的自测清单。不夸张地说每次照着跑完这张表代码出问题的概率至少降低一半。测试场景操作步骤预期结果是否通过首次运行删除 tasks.json 后运行程序正常进入菜单不报错是添加任务连续添加 3 个不同标题的任务每次提示成功列表显示 3 项是空标题直接回车添加任务提示标题不能为空不写入是查看列表选择查看任务显示编号、状态、标题、时间是标记完成选择标记已完成任务的编号状态变为已完成是重复标记再次标记同一个已完成任务程序不崩溃状态不变是删除任务删除中间的一个任务列表不再显示该项是删除不存在的编号输入一个超大编号提示没有找到程序不退出是输入非数字在编号位置输入 abc提示编号必须是数字是重启验证退出后重新运行程序之前添加的任务还在是空数据启动把所有任务删光后重启程序正常查看列表提示空是损坏文件手动把 tasks.json 写成乱码再启动不崩溃或至少有提示视可选择这张表不一定要全部写进提交的文档但你自己必须跑一遍。每跑过一项就在心里打个勾。我见过太多明明功能都写了却因为一次意外输入直接崩给老师看的惨案自测的核心目的就是把这些意外提前炸掉。5. 交作业前的最后 20 分钟代码规范与 README5.1 让老师一眼看懂你的代码功能全部跑通之后还剩最后一个环节也是很多人完全忽略的环节让代码本身会说话。我自己做课程助教的时候批改过接近一百份类似作业老实说一份一眼能看懂的代码和一份需要反复琢磨才能懂的代码分数差距往往比想象中大得多。代码规范不需要背完整个 PEP8记住几条关键的就行。第一函数命名用动词比如 add_task、delete_task、list_tasks一看就知道功能。第二常量用全大写比如 TODO_FILE。第三每个函数前写一行 docstring简单说明参数和返回值。第四不要把几十行逻辑都塞在 main 里main 只负责调用其他函数。第五按固定顺序组织文件import 区、常量区、工具函数、业务函数、main 函数。再分享一个提升代码观感的小技巧在每个函数的开头加一个简短注释说明这段逻辑解决什么问题。注释不是写给老师看的是写给一周后的自己看的。写注释的时候要克制一行能说清楚的事不需要写三行。我见过有人把# 这是一个添加任务的函数这种废话挂在每行代码上面那比没有注释还让人头疼。5.2 写一个 5 分钟能跑通的 README如果作业要求在文档中提交我强烈建议花十分钟写一个 README.md。它的核心受众是老师但受益者是你自己——因为写出 README 的过程本质上就是一次完整的需求梳理和功能盘点。一个课程项目 README 至少包含四块内容项目名称和一句话简介、运行环境说明、启动方式、功能介绍。给你一个可以直接套用的模板结构# 命令行待办事项管理程序 一个基于 Python 的待办事项管理工具支持任务的添加、查看、标记完成、删除和持久化存储。 ## 运行环境 - Python 3.8 - 无需第三方依赖 ## 运行方式 - 在项目根目录执行python main.py ## 功能列表 1. 添加任务输入标题创建新任务 2. 查看任务打印全部任务及状态 3. 标记完成通过编号标记任务为已完成 4. 删除任务通过编号删除任务 5. 数据存储所有数据保存在 tasks.json 中别小看这十分钟它可能直接给你换个好印象。老师批改时可能同时打开几十个文件夹一个能立刻看懂的项目结构比一个纯粹裸奔的 py 文件友好太多。而且 README 写完之后你等于把整个项目的功能重新过了一遍常常会顺手发现某些隐藏 bug。5.3 我的第五次作业复盘最后聊聊我辅导过的那个学弟最终的结局。他选择了我说的重构方案把原本挤在一起的 200 多行代码拆成了数据层、业务层、交互层三层结构。做完之后他跟我说了句很有代表性的话改完之后代码看起来好像变多了但我反而觉得心里踏实了因为我知道每一段代码是干嘛的。这就是第五次作业真正想让你获得的东西。我个人的体会是很多人在做这种阶段性作业时喜欢追求一次写对或最短代码量但这两件事在真实工程里都不是第一优先级。第一优先级永远是结构清楚、出了问题能快速定位。第五次作业的价值就是让你在代价最小的学生时代体验一次从需求到设计再到实现和测试的完整闭环。这个习惯一旦养成后面再遇到写脚本、做课程设计、甚至工作后接一个小项目你都会比同龄人从容得多。如果你现在正卡在第五次作业上别再对着空白的 py 文件发愣了。先拿出一张纸把功能拆开再定数据结构然后一个函数一个函数地填。等到你的程序能在一个干净的环境里从启动到退出都稳稳跑通时你就已经跨过这个分水岭了。