综合实战:用 Python 做一个待办事项管理器(CLI 版)

文章目录

前提知识:模块与包:import 用法与标准库宝藏库巡礼


前九篇文章各有各的知识点------数据类型、函数、异常、类、模块......但它们是分散的。学完容易忘,因为没有机会看到它们在一起工作

本篇的目标是做一个真正能跑的命令行待办事项管理器。把列表、字典、函数、文件操作、异常处理、标准库全部串联起来,成为一个可以每天使用的工具。

知识点串联表

功能 涉及的前置知识点
待办项存储(dict + 列表) #04 列表与字典
每个功能一个函数 #05 函数入门
主循环 + 输入验证 #03 条件判断与循环
文件不存在时自动创建 #06 文件操作 + #07 错误与异常
JSON 数据持久化 #06 json 模块
菜单选项 → 函数映射 #03 + #04 字典做映射
加载标准库工具 #09 模块与包
类封装(可选进阶) #08 类与对象

一、先看架构

在写代码之前,先把程序的结构想清楚。整体是一个菜单驱动的循环程序
1
2
3
4
5
6
7
程序启动
加载 todos.json

(文件不存在则创建空列表)
显示菜单

  1. 添加 / 2. 查看全部

  2. 查看未完成 / 4. 查看已完成

  3. 标记完成 / 6. 删除 / 7. 退出
    接收用户输入
    输入是什么?
    add_todo()
    list_todos()
    complete_todo()
    delete_todo()
    退出程序
    持久化保存

(写入 todos.json)
程序结束

数据存储结构是这样的:

json 复制代码
// todos.json
[
  {
    "id": 1,
    "title": "阅读模块与包文章",
    "completed": false,
    "created_at": "2026-04-27 09:15"
  },
  {
    "id": 2,
    "title": "整理 Python 笔记",
    "completed": true,
    "created_at": "2026-04-26 20:30"
  }
]

二、第一步:内存版------只管添加和查看

先写一个最简单版本,不涉及文件操作。所有待办项存在内存列表里,程序退出就消失------这个版本用来验证数据结构是否合理。

2.1 定义数据结构

每个待办项是一个字典,包含:

  • id:唯一编号,用于精确定位每条记录
  • title:待办内容
  • completed:是否已完成(布尔值)
  • created_at:创建时间
python 复制代码
import datetime

todos = []          # 全局列表,存储所有待办项
next_id = 1        # 下一个可用的 ID


def add_todo(title):
    global next_id
    todo = {
        "id": next_id,
        "title": title,
        "completed": False,
        "created_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
    }
    todos.append(todo)
    next_id += 1
    print(f"✅ 已添加:{title}")


def list_todos():
    if not todos:
        print("📋 暂无待办事项")
        return

    for todo in todos:
        status = "✓" if todo["completed"] else "○"
        print(f"  [{status}] #{todo['id']} {todo['title']}")

2.2 测试这两个函数

python 复制代码
add_todo("阅读模块与包文章")
add_todo("整理 Python 笔记")
add_todo("完成待办管理器 v1")
list_todos()

输出:

复制代码
✅ 已添加:阅读模块与包文章
✅ 已添加:整理 Python 笔记
✅ 已添加:完成待办管理器 v1
  [○] #1 阅读模块与包文章
  [○] #2 整理 Python 笔记
  [○] #3 完成待办管理器 v1

关键理解:到这里,待办项的数据结构已经验证完毕。后续所有工作都是在这些基础上加功能,而不是重新设计数据结构。


三、第二步:加上标记完成和删除

3.1 标记完成

用户输入待办 ID,程序找到对应项并将其 completed 设为 True

python 复制代码
def complete_todo(todo_id):
    for todo in todos:
        if todo["id"] == todo_id:
            if todo["completed"]:
                print(f"⚠️  #{todo_id} 已经是完成状态")
                return
            todo["completed"] = True
            print(f"🎉 #{todo_id} {todo['title']} 已标记完成")
            return
    print(f"❌ 未找到 ID 为 {todo_id} 的待办项")

3.2 删除待办

python 复制代码
def delete_todo(todo_id):
    for i, todo in enumerate(todos):
        if todo["id"] == todo_id:
            removed = todos.pop(i)
            print(f"🗑️  已删除:{removed['title']}")
            return
    print(f"❌ 未找到 ID 为 {todo_id} 的待办项")

注意pop(i) 按索引删除------但查找时用的是 id(内容),删除时用的是 enumerate 获取的索引 i。这两个操作必须配合,不能直接用 id 作为列表索引,因为 ID 是逻辑编号,不等于列表下标。

测试:

python 复制代码
complete_todo(2)
delete_todo(3)
list_todos()

输出:

复制代码
🎉 #2 整理 Python 笔记 已标记完成
🗑️  已删除:完成待办管理器 v1
  [○] #1 阅读模块与包文章
  [✓] #2 整理 Python 笔记

四、第三步:加上 JSON 文件持久化

内存版本的致命问题是程序退出后数据全丢了。加一层文件操作,让数据保存到 todos.json

4.1 保存和加载函数

python 复制代码
import json
import os

FILENAME = "todos.json"


def save_todos():
    """将当前 todos 列表写入 JSON 文件"""
    with open(FILENAME, "w", encoding="utf-8") as f:
        json.dump(todos, f, ensure_ascii=False, indent=2)
    print(f"💾 已保存到 {FILENAME}")


def load_todos():
    """从 JSON 文件加载待办项列表"""
    global next_id
    if not os.path.exists(FILENAME):
        print("📁 首次运行,创建空数据文件")
        return

    with open(FILENAME, "r", encoding="utf-8") as f:
        loaded = json.load(f)

    todos.clear()
    todos.extend(loaded)

    # 恢复 next_id:已有 ID 的最大值 + 1
    if todos:
        next_id = max(t["id"] for t in todos) + 1
    else:
        next_id = 1

    print(f"📂 已从 {FILENAME} 加载 {len(todos)} 条待办")


def add_todo(title):
    global next_id
    todo = {
        "id": next_id,
        "title": title,
        "completed": False,
        "created_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
    }
    todos.append(todo)
    next_id += 1
    save_todos()           # 每次添加后自动保存
    print(f"✅ 已添加:{title}")


def complete_todo(todo_id):
    for todo in todos:
        if todo["id"] == todo_id:
            todo["completed"] = True
            save_todos()
            print(f"🎉 #{todo_id} {todo['title']} 已标记完成")
            return
    print(f"❌ 未找到 ID 为 {todo_id} 的待办项")


def delete_todo(todo_id):
    for i, todo in enumerate(todos):
        if todo["id"] == todo_id:
            removed = todos.pop(i)
            save_todos()
            print(f"🗑️  已删除:{removed['title']}")
            return
    print(f"❌ 未找到 ID 为 {todo_id} 的待办项")

设计说明 :把 save_todos() 嵌入了 add_todocomplete_tododelete_todo 三个函数------这叫"每次修改后立即保存",简单可靠。复杂系统会用事务或缓冲,但这里每条操作都足够轻量,即时保存没有性能问题。


五、第四步:加上主循环和异常处理

5.1 菜单选项 → 函数映射

用字典替代大量 if/elif,是让代码更优雅的实用技巧:

python 复制代码
MENU = """
========================================
         📝 待办事项管理器
========================================
  1. 添加待办
  2. 查看待办
  3. 标记完成
  4. 删除待办
  5. 退出
========================================
请输入选项(1-5):
"""

主循环:

python 复制代码
def main():
    load_todos()

    while True:
        try:
            choice = input(MENU).strip()

            if choice == "1":
                title = input("请输入待办内容:").strip()
                if not title:
                    print("⚠️  内容不能为空")
                    continue
                add_todo(title)

            elif choice == "2":
                list_todos()

            elif choice == "3":
                list_todos()
                if not todos:
                    continue
                try:
                    todo_id = int(input("请输入要完成的待办 ID:").strip())
                except ValueError:
                    print("⚠️  请输入数字 ID")
                    continue
                complete_todo(todo_id)

            elif choice == "4":
                list_todos()
                if not todos:
                    continue
                try:
                    todo_id = int(input("请输入要删除的待办 ID:").strip())
                except ValueError:
                    print("⚠️  请输入数字 ID")
                    continue
                delete_todo(todo_id)

            elif choice == "5":
                print("👋 下次见!")
                break

            else:
                print("⚠️  无效选项,请输入 1-5 之间的数字")

        except KeyboardInterrupt:
            print("\n👋 强制退出,下次见!")
            break


if __name__ == "__main__":
    main()

5.2 异常处理的三处场景

这段代码里有三处不同类型的异常处理,各自解决不同问题:

位置 异常类型 保护什么
int(input(...)) ValueError 用户输入了非数字内容
open(FILENAME, "r") FileNotFoundError 数据文件不存在(load_todos 里已处理)
input() 外层 KeyboardInterrupt 用户按 Ctrl+C 强制退出

六、完整代码

把所有部分组合在一起,加上注释和 docstring:

python 复制代码
"""
待办事项管理器 v1.0
命令行界面,数据持久化到 JSON 文件
"""

import json
import os
import datetime

FILENAME = "todos.json"
todos = []
next_id = 1


def load_todos():
    """从 JSON 文件加载待办项列表,不存在则创建空文件"""
    global next_id
    if not os.path.exists(FILENAME):
        open(FILENAME, "w", encoding="utf-8").close()
        print("📁 首次运行,已创建空数据文件")
        return

    try:
        with open(FILENAME, "r", encoding="utf-8") as f:
            data = json.load(f)
        todos.clear()
        todos.extend(data)
        next_id = (max(t["id"] for t in todos) + 1) if todos else 1
        print(f"📂 已加载 {len(todos)} 条待办")
    except (json.JSONDecodeError, IOError) as e:
        print(f"⚠️  数据文件读取失败,将使用空列表:{e}")


def save_todos():
    """将当前 todos 列表写入 JSON 文件"""
    try:
        with open(FILENAME, "w", encoding="utf-8") as f:
            json.dump(todos, f, ensure_ascii=False, indent=2)
    except IOError as e:
        print(f"❌ 保存失败:{e}")


def add_todo(title):
    """添加一条待办项"""
    global next_id
    todo = {
        "id": next_id,
        "title": title,
        "completed": False,
        "created_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
    }
    todos.append(todo)
    next_id += 1
    save_todos()
    print(f"✅ 已添加 [{todo['id']}] {title}")


def list_todos(filter_status=None):
    """列出所有待办项,支持按状态过滤"""
    if not todos:
        print("📋 暂无待办事项")
        return

    print("\n" + "=" * 40)
    for todo in todos:
        if filter_status is not None and todo["completed"] != filter_status:
            continue
        status = "✓" if todo["completed"] else "○"
        line = f"  [{status}] #{todo['id']} {todo['title']}"
        if todo["completed"]:
            line += f" ({todo['created_at']})"
        print(line)
    print("=" * 40)
    print(f"共 {len(todos)} 条,"
          f"已完成 {sum(1 for t in todos if t['completed'])} 条,"
          f"未完成 {sum(1 for t in todos if not t['completed'])} 条")


def complete_todo(todo_id):
    """将指定 ID 的待办标记为已完成"""
    for todo in todos:
        if todo["id"] == todo_id:
            if todo["completed"]:
                print(f"⚠️  #{todo_id} 已经是完成状态")
                return
            todo["completed"] = True
            save_todos()
            print(f"🎉 #{todo_id} 已标记完成:{todo['title']}")
            return
    print(f"❌ 未找到 ID 为 {todo_id} 的待办项")


def delete_todo(todo_id):
    """删除指定 ID 的待办项"""
    for i, todo in enumerate(todos):
        if todo["id"] == todo_id:
            removed = todos.pop(i)
            save_todos()
            print(f"🗑️  已删除:{removed['title']}")
            return
    print(f"❌ 未找到 ID 为 {todo_id} 的待办项")


def main():
    load_todos()

    MENU = """
========================================
         📝 待办事项管理器 v1.0
========================================
  1. 添加待办
  2. 查看全部
  3. 查看未完成
  4. 查看已完成
  5. 标记完成
  6. 删除待办
  7. 退出
========================================
    """

    while True:
        try:
            choice = input(MENU).strip()

            if choice == "1":
                title = input("请输入待办内容:").strip()
                if not title:
                    print("⚠️  内容不能为空")
                    continue
                add_todo(title)

            elif choice == "2":
                list_todos()

            elif choice == "3":
                list_todos(filter_status=False)

            elif choice == "4":
                list_todos(filter_status=True)

            elif choice == "5":
                list_todos(filter_status=False)
                if not todos:
                    continue
                raw = input("请输入要完成的待办 ID(直接回车取消):").strip()
                if not raw:
                    continue
                try:
                    complete_todo(int(raw))
                except ValueError:
                    print("⚠️  请输入数字")

            elif choice == "6":
                list_todos()
                if not todos:
                    continue
                raw = input("请输入要删除的待办 ID(直接回车取消):").strip()
                if not raw:
                    continue
                try:
                    delete_todo(int(raw))
                except ValueError:
                    print("⚠️  请输入数字")

            elif choice == "7":
                print("👋 下次见!")
                break

            else:
                print("⚠️  无效选项,请输入 1-7")

        except KeyboardInterrupt:
            print("\n👋 下次见!")
            break


if __name__ == "__main__":
    main()

七、运行效果

text 复制代码
$ python todo_manager.py
📂 已加载 3 条待办

========================================
         📝 待办事项管理器 v1.0
========================================
  1. 添加待办
  2. 查看全部
  3. 查看未完成
  4. 查看已完成
  5. 标记完成
  6. 删除待办
  7. 退出
========================================

请输入选项(1-7):
2

========================================
  [○] #1 阅读模块与包文章
  [✓] #2 整理 Python 笔记
  [○] #3 完成待办管理器 v1
========================================
共 3 条,已完成 1 条,未完成 2 条

请输入选项(1-7):
1
请输入待办内容:发布文章到 CSDN
💾 已保存到 todos.json
✅ 已添加 [4] 发布文章到 CSDN

请输入选项(1-7):
5

========================================
  [○] #1 阅读模块与包文章
  [○] #3 完成待办管理器 v1
  [○] #4 发布文章到 CSDN
========================================
共 3 条,已完成 0 条,未完成 3 条

请输入要完成的待办 ID(直接回车取消):3
💾 已保存到 todos.json
🎉 #3 已标记完成:完成待办管理器 v1

请输入选项(1-7):
7
👋 下次见!

程序退出后,todos.json 文件里已经保存了所有数据,下次运行会自动加载:

json 复制代码
[
  {
    "id": 1,
    "title": "阅读模块与包文章",
    "completed": false,
    "created_at": "2026-04-27 09:15"
  },
  {
    "id": 2,
    "title": "整理 Python 笔记",
    "completed": true,
    "created_at": "2026-04-26 20:30"
  },
  {
    "id": 3,
    "title": "完成待办管理器 v1",
    "completed": true,
    "created_at": "2026-04-27 10:00"
  },
  {
    "id": 4,
    "title": "发布文章到 CSDN",
    "completed": false,
    "created_at": "2026-04-27 10:05"
  }
]

八、扩展方向:让工具更接近真实产品

当前版本可以正常工作,但真实场景中还需要更多功能。以下是几个典型的扩展方向,每个都能独立成一个小专题:
v1.0 当前版本
v2.0 优先级支持

(高/中/低 三档)
v2.0 截止日期

(datetime + 排序)
v2.0 分类标签

(多对多关系)
v2.0 命令行参数

(sys.argv 入口)
v2.0 数据统计

(完成率/趋势)

扩展功能 涉及的新知识点
优先级(高/中/低) 字典增加 priority 字段,列表排序 sort(key=...)
截止日期 + 排序 datetime 模块,按日期排序
分类标签 列表的列表或 set,支持多标签
命令行参数入口 sys.argvpython todo.py add "内容"
数据统计 汇总计算、格式化输出

九、知识结构图

毕业项目:待办管理器
数据结构

dict + list
函数划分

每个功能一个函数
主循环

while + try/except
JSON 持久化

json.dump/load
文件异常处理

FileNotFoundError
菜单字典映射

选项 → 函数
标准库工具

datetime/os


十、工程原则

  1. 数据结构先行:先确定 JSON 存储结构,再写代码------改数据结构代价很高,中途发现结构不对再改会推翻很多代码。
  2. 每个功能一个函数add_todolist_todoscomplete_tododelete_todo 各司其职,主循环只负责分发任务,不写具体逻辑。
  3. 每次修改立即保存:这个工具的数据量极小(几百条),每次操作后保存没有性能问题,却能保证程序崩溃时数据不丢失。
  4. 异常处理分散到各处,而非集中load_todos 处理文件不存在,main 的循环处理用户输入格式错误和强制退出------每个场景独立处理,不需要一个大 try 包住整个程序。
  5. 用字典做分支映射:菜单选项 → 函数映射是一个值得养成的习惯------加新选项只需要在字典里加一行,主循环保持整洁。

到这里,从变量到函数,从数据结构到文件操作,从异常处理到模块化设计------这一路走下来,已经具备了独立编写小型 Python 程序的能力。

如果觉得这篇文章有帮助,欢迎点赞、评论和关注。前面的模块与包、类与对象、错误与异常等文章中也有类似的综合实战,可以结合阅读,温故知新。

相关推荐
永远睡不够的入2 小时前
C++11新特性(3):lambda不是玄学:从编译器生成的仿函数类彻底搞懂 C++ 匿名函数
开发语言·c++
HAPPY酷2 小时前
UE5 C++ 避坑指南:暴力移除 Electronic Nodes 插件,回归纯净开发
开发语言·c++·ue5
huipeng9262 小时前
分布式服务部署详解
java·开发语言·spring cloud·微服务
步辞2 小时前
CSS如何对表单输入框获取焦点时实现标签上浮过渡
jvm·数据库·python
eqwaak02 小时前
4 月技术快讯|Rust 1.90 正式发布,系统级开发再进化
开发语言·后端·rust
小此方2 小时前
Re:思考·重建·记录 现代C++ C++11篇 (四)C++ Lambda 全解析:编译器是如何为你生成仿函数的?
开发语言·c++·c++11·现代c++
秦歌6662 小时前
RAG-6-高级RAG实战案例:自适应路由 + 自评估重写 + 网络回退
java·服务器·前端·人工智能·python
Brilliantwxx2 小时前
【C++】初认识模版
开发语言·c++
财经资讯数据_灵砚智能2 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年4月27日
人工智能·python·信息可视化·自然语言处理·ai编程