文档
Python 实战篇:构建命令行待办事项应用
前言
学完基础语法后,最好的巩固方式就是动手做一个完整的项目。这篇教程带你从零构建一个命令行待办事项(Todo)应用,涵盖文件持久化、命令解析、错误处理等真实场景。
第 1 章:需求分析
功能列表
- 添加任务:
python todo.py add "买牛奶" - 列出所有任务:
python todo.py list - 完成任务:
python todo.py done 1(按编号) - 删除任务:
python todo.py delete 1 - 数据持久化到 JSON 文件
项目结构
todo/
├── todo.py # 入口 + 命令行解析
├── models.py # 数据模型
├── storage.py # JSON 文件读写
└── tasks.json # 持久化文件(运行时生成)
第 2 章:数据模型
创建 models.py:
"""数据模型模块"""
from dataclasses import dataclass, field, asdict
from datetime import datetime
from typing import Optional
@dataclass
class Task:
"""任务数据类"""
title: str
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
done: bool = False
done_at: Optional[str] = None
def mark_done(self) -> None:
"""标记完成"""
self.done = True
self.done_at = datetime.now().isoformat()
def to_dict(self) -> dict:
"""转为字典(用于 JSON 序列化)"""
return asdict(self)
@classmethod
def from_dict(cls, data: dict) -> "Task":
"""从字典恢复"""
return cls(**data)
第 3 章:持久化层
创建 storage.py:
"""JSON 文件存储模块"""
import json
from pathlib import Path
from typing import List
from models import Task
class TaskStorage:
"""任务的 JSON 文件存储"""
def __init__(self, filepath: str = "tasks.json"):
self.filepath = Path(filepath)
def load(self) -> List[Task]:
"""从文件加载所有任务"""
if not self.filepath.exists():
return []
try:
data = json.loads(self.filepath.read_text(encoding="utf-8"))
return [Task.from_dict(item) for item in data]
except (json.JSONDecodeError, KeyError) as e:
print(f"⚠️ 数据文件损坏: {e},将创建新文件")
return []
def save(self, tasks: List[Task]) -> None:
"""保存所有任务到文件"""
data = [task.to_dict() for task in tasks]
self.filepath.write_text(
json.dumps(data, ensure_ascii=False, indent=2),
encoding="utf-8"
)
第 4 章:业务逻辑
创建 todo_manager.py:
"""任务管理业务逻辑"""
from typing import List
from models import Task
from storage import TaskStorage
class TodoManager:
"""待办事项管理器"""
def __init__(self, storage: TaskStorage):
self.storage = storage
self.tasks: List[Task] = self.storage.load()
def add(self, title: str) -> Task:
"""添加新任务"""
task = Task(title=title)
self.tasks.append(task)
self.storage.save(self.tasks)
return task
def list_all(self, show_done: bool = True) -> List[tuple[int, Task]]:
"""列出任务,返回 (序号, 任务) 列表"""
result = []
for i, task in enumerate(self.tasks, start=1):
if show_done or not task.done:
result.append((i, task))
return result
def mark_done(self, index: int) -> Task:
"""将指定任务标记为完成"""
task = self._get_task(index)
if task.done:
raise ValueError(f"任务 {index} 已经完成了")
task.mark_done()
self.storage.save(self.tasks)
return task
def delete(self, index: int) -> Task:
"""删除指定任务"""
task = self._get_task(index)
removed = self.tasks.pop(index - 1)
self.storage.save(self.tasks)
return removed
def _get_task(self, index: int) -> Task:
"""根据用户看到的序号获取任务"""
if index < 1 or index > len(self.tasks):
raise IndexError(
f"任务编号 {index} 无效,当前共 {len(self.tasks)} 个任务"
)
return self.tasks[index - 1]
第 5 章:命令行入口
创建 todo.py:
#!/usr/bin/env python3
"""命令行待办事项应用 — 入口"""
import sys
from pathlib import Path
from storage import TaskStorage
from todo_manager import TodoManager
DATA_FILE = Path(__file__).parent / "tasks.json"
def print_task(index: int, task, color: bool = True):
"""格式化打印一个任务"""
status = "✅" if task.done else "⬜"
done_info = f" (完成于 {task.done_at[:19]})" if task.done else ""
print(f" {status} [{index}] {task.title}{done_info}")
def print_usage():
"""打印使用说明"""
print("""用法:
python todo.py add <任务内容> 添加新任务
python todo.py list 列出所有任务
python todo.py done <编号> 完成任务
python todo.py delete <编号> 删除任务
python todo.py help 显示此帮助""")
def main():
if len(sys.argv) < 2:
print_usage()
return
command = sys.argv[1].lower()
storage = TaskStorage(str(DATA_FILE))
manager = TodoManager(storage)
try:
if command == "add":
if len(sys.argv) < 3:
print("❌ 请提供任务内容")
return
title = " ".join(sys.argv[2:])
task = manager.add(title)
print(f"✅ 已添加: {task.title}")
elif command == "list":
tasks = manager.list_all()
if not tasks:
print("📭 暂无任务,用 add 命令添加吧")
return
print(f"\n📋 待办事项 (共 {len(tasks)} 项):")
for i, task in tasks:
print_task(i, task)
elif command == "done":
if len(sys.argv) < 3:
print("❌ 请提供任务编号")
return
index = int(sys.argv[2])
task = manager.mark_done(index)
print(f"🎉 已完成: {task.title}")
elif command == "delete":
if len(sys.argv) < 3:
print("❌ 请提供任务编号")
return
index = int(sys.argv[2])
task = manager.delete(index)
print(f"🗑️ 已删除: {task.title}")
elif command in ("help", "--help", "-h"):
print_usage()
else:
print(f"❌ 未知命令: {command}")
print_usage()
except (IndexError, ValueError) as e:
print(f"❌ 错误: {e}")
except Exception as e:
print(f"❌ 意外错误: {e}")
raise
if __name__ == "__main__":
main()
第 6 章:运行与测试
# 进入项目目录
cd todo/
# 添加任务
python todo.py add "学习 Python 装饰器"
python todo.py add "写周报"
python todo.py add "买菜"
# 查看列表
python todo.py list
# 完成第一个任务
python todo.py done 1
# 再次查看(观察变化)
python todo.py list
# 删除已完成的任务
python todo.py delete 1
# 最终查看
python todo.py list
预期输出
✅ 已添加: 学习 Python 装饰器
✅ 已添加: 写周报
✅ 已添加: 买菜
📋 待办事项 (共 3 项):
⬜ [1] 学习 Python 装饰器
⬜ [2] 写周报
⬜ [3] 买菜
🎉 已完成: 学习 Python 装饰器
📋 待办事项 (共 3 项):
✅ [1] 学习 Python 装饰器 (完成于 2024-...)
⬜ [2] 写周报
⬜ [3] 买菜
🗑️ 已删除: 学习 Python 装饰器
📋 待办事项 (共 2 项):
⬜ [1] 写周报
⬜ [2] 买菜
第 7 章:扩展练习
试试自己实现以下功能:
- 优先级:任务增加
priority字段(高/中/低),list按优先级排序 - 搜索:
python todo.py search "Python"搜索标题包含关键词的任务 - 归档:已完成的超过 7 天的任务自动移到
archive.json - 颜色输出:用
colorama或 ANSI 转义序列让输出更美观 - SQLite 存储:将
TaskStorage替换为 SQLite 实现
提示:使用 argparse 模块替代手动解析 sys.argv 可以让命令解析更专业。
思考题
- 如果多个用户同时操作
tasks.json,会发生什么问题?如何解决? json.dumps的ensure_ascii=False有什么用?- 为什么
DATA_FILE用Path(__file__).parent / "tasks.json"而不是直接"tasks.json"? - 代码中哪些地方用到了「鸭子类型」?
提示:1. 竞态条件 → 文件锁或数据库;2. 保证中文正常显示;3. 确保文件在脚本所在目录,不受运行目录影响;4.
TaskStorage和TodoManager之间只依赖接口而非具体实现。