基础语法05_待办清单练习与答案
从零实现待办清单,综合练习 Python 基础语法并编写测试。
Python 基础语法 05:待办清单练习与答案
学习目标:把变量、列表、条件、循环、函数、类和异常组合成一个能运行的小程序。只使用 Python 标准库;初学者应先自己尝试,再对照完整答案。
1. 本章要完成什么
程序支持四个命令:add 标题 添加任务,list 列出任务,done 编号 完成任务,quit 退出。任务编号从 1 开始。输入空标题、无效编号或未知命令时给出提示,程序不崩溃。为了集中练语法,数据仅保存在内存中,退出后消失。
在新建的练习目录保存为 todo.py,运行 python3 todo.py。不要把练习文件直接放进个人笔记目录,也不要用私人笔记作为测试数据。
2. 先把需求拆成小函数
Task 表示一条任务;add_task 只负责校验和添加;complete_task 只负责按编号修改;render_tasks 负责生成显示文本。交互循环负责读取命令和打印反馈。把规则从 input() 分离后,不开终端也能直接测试核心函数。
建议自己先写:先实现 add 与 list,运行成功后再加 done 与错误处理。每加一项功能都试一次正常输入和一次错误输入。
3. 完整可运行答案
from dataclasses import dataclass
@dataclass
class Task:
title: str
done: bool = False
def add_task(tasks: list[Task], title: str) -> None:
cleaned = title.strip()
if not cleaned:
raise ValueError("任务标题不能为空")
tasks.append(Task(cleaned))
def complete_task(tasks: list[Task], number: int) -> None:
if number < 1 or number > len(tasks):
raise ValueError("任务编号不存在")
tasks[number - 1].done = True
def render_tasks(tasks: list[Task]) -> str:
if not tasks:
return "还没有任务"
lines: list[str] = []
for number, task in enumerate(tasks, start=1):
mark = "✓" if task.done else " "
lines.append(f"{number}. [{mark}] {task.title}")
return "\n".join(lines)
def main() -> None:
tasks: list[Task] = []
print("命令:add 标题 | list | done 编号 | quit")
while True:
try:
line = input("> ").strip()
except EOFError:
print("\n再见")
return
if not line:
continue
command, *rest = line.split(maxsplit=1)
try:
if command == "add":
add_task(tasks, rest[0] if rest else "")
print("已添加")
elif command == "list":
print(render_tasks(tasks))
elif command == "done":
if not rest:
raise ValueError("请提供任务编号")
try:
number = int(rest[0])
except ValueError as exc:
raise ValueError("任务编号必须是整数") from exc
complete_task(tasks, number)
print("已完成")
elif command == "quit":
print("再见")
return
else:
print("未知命令")
except ValueError as exc:
print(f"输入错误:{exc}")
if __name__ == "__main__":
main()
4. 逐步看懂关键语句
@dataclass自动生成初始化方法,所以Task(cleaned)会创建一条新任务;done的默认值为False。tasks: list[Task] = []声明一个列表,类型提示让阅读者知道元素是什么;运行时不会自动强制检查。while True持续接收命令,return退出main,continue跳到下一轮。command, *rest = ...把第一个词给命令,把可能剩下的标题或编号放到列表中;没有第二段时rest为空列表。enumerate(tasks, start=1)产生从 1 开始的显示编号,列表索引仍从 0 开始,所以完成任务用number - 1。- 内层
try只捕获整数转换错误并加上更清楚的业务说明;外层try把可预期的ValueError变成用户可读消息。 if __name__ == "__main__"保证导入todo.py进行测试时不会启动交互循环。
5. 一次预期运行
命令:add 标题 | list | done 编号 | quit
> list
还没有任务
> add 学习 Python
已添加
> add
输入错误:任务标题不能为空
> list
1. [ ] 学习 Python
> done 1
已完成
> list
1. [✓] 学习 Python
> quit
再见
如果你的输出不同,先确认运行的是刚保存的文件;再检查缩进、是否把 done 写成字符串、编号是否减 1。遇到异常时从报错末行读错误类型,再回到提示的代码行,而不是一次改很多处。
6. 不运行交互也能测试规则
把下面代码保存为同目录的 test_todo.py,执行 python3 -m unittest test_todo.py。它只调用函数,不依赖输入顺序或终端。
import unittest
from todo import Task, add_task, complete_task, render_tasks
class TodoTests(unittest.TestCase):
def test_add_and_complete(self) -> None:
tasks: list[Task] = []
add_task(tasks, " 学习 Go ")
self.assertEqual(tasks[0].title, "学习 Go")
complete_task(tasks, 1)
self.assertTrue(tasks[0].done)
def test_rejects_blank_title(self) -> None:
with self.assertRaises(ValueError):
add_task([], " ")
def test_empty_list(self) -> None:
self.assertEqual(render_tasks([]), "还没有任务")
if __name__ == "__main__":
unittest.main()
7. 自己扩展与答案提示
- 加
delete 编号:检查范围后用tasks.pop(number - 1);删除后编号会重新排列,这正是当前内存版的规则。 - 加
count:统计未完成任务。答案可用sum(not task.done for task in tasks);先用普通循环实现更有助于理解。 - 把任务保存为 JSON:先把
Task转成只含title、done的字典,再写到新建学习目录。读回时逐字段校验,坏文件不能静默当作空列表。
学习提示:能独立解释每个分支和错误路径,再给程序加文件持久化。先让内存版正确,比一次写完复杂版本更容易排错。