← Backend / Python

基础语法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. 逐步看懂关键语句

  1. @dataclass 自动生成初始化方法,所以 Task(cleaned) 会创建一条新任务;done 的默认值为 False。
  2. tasks: list[Task] = [] 声明一个列表,类型提示让阅读者知道元素是什么;运行时不会自动强制检查。
  3. while True 持续接收命令,return 退出 main,continue 跳到下一轮。
  4. command, *rest = ... 把第一个词给命令,把可能剩下的标题或编号放到列表中;没有第二段时 rest 为空列表。
  5. enumerate(tasks, start=1) 产生从 1 开始的显示编号,列表索引仍从 0 开始,所以完成任务用 number - 1。
  6. 内层 try 只捕获整数转换错误并加上更清楚的业务说明;外层 try 把可预期的 ValueError 变成用户可读消息。
  7. 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. 自己扩展与答案提示

  1. 加 delete 编号:检查范围后用 tasks.pop(number - 1);删除后编号会重新排列,这正是当前内存版的规则。
  2. 加 count:统计未完成任务。答案可用 sum(not task.done for task in tasks);先用普通循环实现更有助于理解。
  3. 把任务保存为 JSON:先把 Task 转成只含 title、done 的字典,再写到新建学习目录。读回时逐字段校验,坏文件不能静默当作空列表。
学习提示:能独立解释每个分支和错误路径,再给程序加文件持久化。先让内存版正确,比一次写完复杂版本更容易排错。