news 2026/10/17 14:24:15

Python CLI待办事项项目实战:本地存储与命令行交互

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python CLI待办事项项目实战:本地存储与命令行交互

简介:这是一份面向Python初学者的命令行待办事项练习资源,帮助读者在本地环境中掌握CLI应用的基本开发流程。资源围绕增删任务、列表打印、数据持久化等核心功能展开,要求实现add_one_task、print_list、delete_task、save_todos、load_todos等函数,适合刚接触Python、希望用小型项目巩固文件读写与命令行交互的开发者练手。压缩包共7个文件,约295KB,包含2个py源码文件、1个json配置、1个csv数据文件、1个gitignore、1个md说明文档和1个gif演示图,结构轻量,便于快速上手。已有355人学习关注。通过这份资源,读者可以拿到完整的练习骨架与测试脚本,对照README理解需求后逐步补全功能,借助test.py验证实现是否正确,并参考演示动图直观了解程序运行效果,从而在动手实践中熟悉命令行参数处理、任务数据的保存与加载等常见知识点。

1. 一个本地待办 CLI 的定位:为什么我最终把它留在了工具箱里

很多人第一次写 Python 练手项目,都会选待办事项列表。我见过太多版本:有的用 Tkinter 弹窗,有的用 Flask 起服务,有的直接往 JSON 文件里塞字典。但真正让我愿意留在终端里天天用的,是一个纯命令行、数据只存本地、不依赖任何网络服务的版本。Exercise-todo-list-cli-python就是这样一个项目:它把增删改查、状态标记、优先级排序这些动作全部收敛到几条命令里,数据落在本地文件,关掉终端再打开,任务还在。它适合两类人:一类是刚学完 Python 基础语法,想找一个能跑通完整流程的练手项目;另一类是不想被各种云同步、账号登录打扰,只想在本地快速记事的开发者。我第一次跑它的时候,最直观的感受是:没有数据库、没有前端、没有依赖注入,就是一个脚本加一个数据文件,但该有的交互逻辑一样不少。

2. 项目结构拆解:从入口脚本到数据持久化层

2.1 目录布局与模块职责

拿到源码后,我习惯先看目录。这个项目的结构很扁平,没有过度设计,常见做法是下面这样:

Exercise-todo-list-cli-python/ ├── main.py # 入口,负责解析命令行参数 ├── todo/ │ ├── __init__.py │ ├── manager.py # 任务管理核心逻辑 │ ├── storage.py # 本地文件读写 │ └── models.py # 任务数据结构定义 ├── data/ │ └── tasks.json # 运行时生成的任务数据文件 ├── requirements.txt # 依赖清单(通常为空或只有标准库) └── README.md

main.py只做一件事:把sys.argv里的命令和参数翻译成对manager的调用。manager.py是大脑,负责校验参数、组装任务对象、决定调用哪个存储方法。storage.py是黑匣子,只关心怎么把任务列表序列化成 JSON 再写回磁盘。models.py定义任务长什么样,通常是一个Task类或者TypedDict。这种分层的好处是:你想换存储方式,只改storage.py;想加新命令,只动main.py和manager.py,互不干扰。

2.2 任务数据模型与字段设计

任务对象是整个项目的核心数据结构。我拆过几个类似项目,字段设计大同小异,但有几个细节决定了后面好不好扩展:

# todo/models.py from dataclasses import dataclass, field from datetime import datetime from typing import Optional @dataclass class Task: id: int # 唯一标识,通常用递增整数 title: str # 任务标题,必填 done: bool = False # 完成状态,默认未完成 priority: int = 2 # 优先级:1 高,2 中,3 低 created_at: str = field(default_factory=lambda: datetime.now().isoformat()) finished_at: Optional[str] = None # 完成时间,未完成时为 None def to_dict(self) -> dict: # 转成可 JSON 序列化的字典 return { "id": self.id, "title": self.title, "done": self.done, "priority": self.priority, "created_at": self.created_at, "finished_at": self.finished_at, }

这里用dataclass而不是普通字典,好处是字段有默认值、类型提示清晰,后面加字段不容易漏。id用递增整数而不是 UUID,是因为本地单用户场景下,递增 ID 更短、更好输入。priority用整数而不是字符串,排序时直接sorted(tasks, key=lambda t: t.priority)就行,不用额外映射。created_at和finished_at用 ISO 格式字符串,JSON 读写不用额外转换,人也能直接看懂。

2.3 存储层:JSON 文件的读写与原子性

本地存储最怕的是写一半程序崩了,文件损坏。常见做法是写临时文件再替换:

# todo/storage.py import json import os import tempfile from typing import List from .models import Task DATA_FILE = os.path.join(os.path.dirname(__file__), "..", "data", "tasks.json") def load_tasks() -> List[Task]: if not os.path.exists(DATA_FILE): return [] with open(DATA_FILE, "r", encoding="utf-8") as f: raw = json.load(f) return [Task(**item) for item in raw] def save_tasks(tasks: List[Task]) -> None: os.makedirs(os.path.dirname(DATA_FILE), exist_ok=True) # 先写临时文件,再原子替换,避免写一半崩溃导致数据丢失 fd, tmp_path = tempfile.mkstemp(dir=os.path.dirname(DATA_FILE)) try: with os.fdopen(fd, "w", encoding="utf-8") as f: json.dump([t.to_dict() for t in tasks], f, ensure_ascii=False, indent=2) os.replace(tmp_path, DATA_FILE) except Exception: if os.path.exists(tmp_path): os.remove(tmp_path) raise

load_tasks里如果文件不存在就返回空列表,这样第一次运行不会报错。save_tasks用tempfile.mkstemp在同一个目录下建临时文件,写完再os.replace替换原文件。os.replace在大多数操作系统上是原子操作,要么旧文件还在,要么新文件完整,不会出现半截 JSON。ensure_ascii=False让中文标题直接可读,indent=2方便手动查看和调试。

3. 命令行交互实现:参数解析与核心增删改查

3.1 用 argparse 搭出子命令骨架

命令行工具最怕参数解析写得乱。Python 标准库的argparse支持子命令,正好适合这种多动作场景:

# main.py import argparse from todo.manager import TaskManager def build_parser(): parser = argparse.ArgumentParser(description="本地待办事项 CLI") sub = parser.add_subparsers(dest="command", required=True) # 添加任务 p_add = sub.add_parser("add", help="添加新任务") p_add.add_argument("title", help="任务标题") p_add.add_argument("-p", "--priority", type=int, choices=[1, 2, 3], default=2) # 列出任务 p_list = sub.add_parser("list", help="列出任务") p_list.add_argument("--all", action="store_true", help="包含已完成任务") # 标记完成 p_done = sub.add_parser("done", help="标记任务完成") p_done.add_argument("id", type=int) # 删除任务 p_del = sub.add_parser("delete", help="删除任务") p_del.add_argument("id", type=int) return parser def main(): parser = build_parser() args = parser.parse_args() mgr = TaskManager() if args.command == "add": mgr.add(args.title, args.priority) elif args.command == "list": mgr.list(show_all=args.all) elif args.command == "done": mgr.mark_done(args.id) elif args.command == "delete": mgr.delete(args.id) if __name__ == "__main__": main()

sub.add_parser为每个动作建一个独立解析器,required=True保证必须输入子命令。add的title是位置参数,直接跟在命令后面;-p是可选参数,限制只能选 1、2、3。list的--all是布尔开关,默认只显示未完成任务。done和delete都接收一个整数id。这样写的好处是python main.py add "写周报" -p 1和python main.py done 3都能被正确解析,而且--help自动生成。

3.2 任务管理器的核心逻辑

TaskManager把存储层和命令行层隔开,所有业务规则集中在这里:

# todo/manager.py from typing import List from .models import Task from .storage import load_tasks, save_tasks class TaskManager: def __init__(self): self.tasks: List[Task] = load_tasks() def _next_id(self) -> int: return max((t.id for t in self.tasks), default=0) + 1 def add(self, title: str, priority: int = 2) -> None: task = Task(id=self._next_id(), title=title, priority=priority) self.tasks.append(task) save_tasks(self.tasks) print(f"已添加 #{task.id} {task.title}") def list(self, show_all: bool = False) -> None: items = self.tasks if show_all else [t for t in self.tasks if not t.done] # 按优先级升序、创建时间升序排列 items.sort(key=lambda t: (t.priority, t.created_at)) if not items: print("没有待办任务") return for t in items: flag = "✓" if t.done else " " print(f"[{flag}] #{t.id} P{t.priority} {t.title}") def mark_done(self, task_id: int) -> None: for t in self.tasks: if t.id == task_id: t.done = True from datetime import datetime t.finished_at = datetime.now().isoformat() save_tasks(self.tasks) print(f"已完成 #{t.id}") return print(f"未找到 #{task_id}") def delete(self, task_id: int) -> None: before = len(self.tasks) self.tasks = [t for t in self.tasks if t.id != task_id] if len(self.tasks) == before: print(f"未找到 #{task_id}") else: save_tasks(self.tasks) print(f"已删除 #{task_id}")

_next_id用max(..., default=0) + 1,即使删掉中间任务,新 ID 也不会重复。add里先构造Task再追加,最后统一save_tasks。list默认过滤掉已完成任务,排序规则是优先级数字小的在前,同优先级按创建时间早的在前。mark_done找到对应 ID 后设置done=True并记录完成时间,然后立即保存。delete用列表推导过滤,如果长度没变说明 ID 不存在,给出提示。这些逻辑都不复杂,但每一步都对应一个明确的用户动作。

3.3 运行方式与依赖说明

这个项目通常只依赖 Python 标准库,不需要额外安装包。跑起来的方式:

# 进入项目根目录 cd Exercise-todo-list-cli-python # 添加任务 python main.py add "整理会议纪要" -p 1 # 查看未完成任务 python main.py list # 查看全部任务 python main.py list --all # 标记完成 python main.py done 1 # 删除任务 python main.py delete 2

如果requirements.txt里列了click或rich之类的库,那就先pip install -r requirements.txt。但大多数这类练手项目为了降低门槛,会刻意只用标准库。data/tasks.json不需要手动创建,第一次add时save_tasks会自动建目录和文件。如果你想把数据放到别处,改storage.py里的DATA_FILE路径就行。

4. 避坑与常见问题:本地文件读写和参数解析的翻车现场

4.1 中文标题写入后变成乱码

现象:add "写周报"之后打开tasks.json,看到的是\u5199\u5468\u62a5这样的转义序列。原因:json.dump默认ensure_ascii=True,会把非 ASCII 字符转义。解决:在save_tasks里加ensure_ascii=False,同时open时指定encoding="utf-8"。这个坑我踩过不止一次,尤其是 Windows 上默认编码不是 UTF-8 的时候,读写都要显式指定。

4.2 任务 ID 重复导致 done 和 delete 误操作

现象:删掉中间某个任务后,再添加新任务,新任务的 ID 和已有任务撞了。原因:如果_next_id用len(self.tasks) + 1,删除后长度变小,新 ID 就会重复。解决:用max((t.id for t in self.tasks), default=0) + 1,保证新 ID 永远比现有最大 ID 大。这个逻辑在manager.py里只有一行,但少了它后面全是玄学 bug。

4.3 并发执行时数据被覆盖

现象:开两个终端同时add,后保存的会把先保存的覆盖掉。原因:load_tasks在TaskManager.__init__时只读一次,两个进程各自持有旧列表,各自save_tasks时互相覆盖。解决:本地单用户场景下,常见做法是每次操作前重新load_tasks,或者加一个简单的文件锁。如果不想引入fcntl或filelock,至少可以在save_tasks前重新读一次再合并。这个项目定位是本地单用户,所以原作者可能没处理,但你要二次开发就得注意。

4.4 argparse 子命令报错信息不友好

现象:输入python main.py不带任何子命令,直接抛error: the following arguments are required: command。原因:required=True强制要求子命令。解决:如果想让不带参数时显示帮助而不是报错,可以把required去掉,然后在main里判断if not args.command: parser.print_help()。另外,add的title如果包含空格,记得用引号包起来,否则会被拆成多个参数。

4.5 数据文件路径写死导致换目录就找不到

现象:把项目挪到另一个文件夹,运行时报FileNotFoundError。原因:DATA_FILE用了相对路径,依赖当前工作目录。解决:用os.path.dirname(__file__)拼绝对路径,这样不管从哪个目录执行python /path/to/main.py,数据文件位置都固定。上面storage.py里的DATA_FILE就是这么写的。

5. 二次开发改造:从单文件到可扩展任务系统的几个切入点

这个项目本身不复杂,但正因为它简单,反而适合拿来练手改造。我一般会从下面几个方向入手,每个方向都能让你对 Python 工程化多一层理解。

第一个切入点是存储格式升级。JSON 文件在任务量少的时候没问题,但如果你想加全文搜索、按标签过滤,每次全量加载再过滤就有点笨。常见做法是换成 SQLite,用标准库sqlite3就行,不用装额外依赖。改造时把storage.py里的load_tasks和save_tasks换成SELECT和INSERT/UPDATE/DELETE,TaskManager的接口保持不变。这样上层命令完全不用动,这就是分层的好处。

第二个切入点是命令扩展。现在只有add、list、done、delete四个动作,你可以加edit改标题、search按关键词过滤、stats统计完成率。加edit的时候注意:argparse里可以给同一个子命令加多个可选参数,比如--title和--priority,然后在manager里判断哪些字段需要更新。加search的时候,如果数据还在 JSON 里,就用列表推导加in判断;如果换了 SQLite,就写LIKE查询。

第三个切入点是输出美化。纯文本的[ ] #1 P2 写周报能看,但不够直观。你可以引入rich库,用表格输出,优先级高的标红,已完成的任务加删除线。改造时只在manager.list里改打印逻辑,不碰存储和解析。如果不想引入第三方库,也可以用 ANSI 转义码手动上色,但 Windows 终端需要额外处理。

第四个切入点是配置化。现在优先级只有 1、2、3 三档,数据文件路径也写死在代码里。你可以加一个config.json,让用户自定义优先级档位、数据文件位置、默认排序方式。读取配置的代码放在TaskManager.__init__里,用json.load读,缺失字段给默认值。这样别人拿到你的分支,不用改代码就能调整行为。

第五个切入点是测试。这个项目非常适合写单元测试,因为storage和manager都可以独立测试。用pytest加tmp_pathfixture,把DATA_FILE指向临时目录,然后测add之后load_tasks能不能读回来、delete不存在的 ID 会不会报错、mark_done有没有正确设置finished_at。我自己的习惯是:每次改完manager.py,先跑一遍测试再手动敲命令验证。从那以后我每次给这类小工具加功能,都强制先写一个最小测试用例,哪怕只覆盖一条路径,也比纯靠手敲强。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 14:24:14

Git误操作急救指南:reflog与fsck挽救丢失代码

1. 急救思路:先搞清楚还能不能救 Git 误操作,现实中多半不是“不会用”,而是“太顺手”。平时提交、切分支、拉代码都是肌肉记忆,一忙起来脑子还没跟上手指,手一滑就是事故。我在团队里见过太多这样的场面:…

作者头像 李华
网站建设 2026/10/17 14:23:49

Agent Skills实战指南:从设计到落地的完整技能封装方法

最近一直在捣鼓 Agent Skills,发现网上的讨论大多数停在“什么是技能”这种概念层,真正能把一个自定义 Skill 从设计到落地讲清楚的太少。这玩意儿说白了就是给 Agent 配上一套“可复用、可组合、可测试”的任务级能力包,让模型不只靠临场发挥…

作者头像 李华
网站建设 2026/10/17 14:23:27

HR智能体落地实操:两周跑通AI提效流程

前阵子和一位做了八年招聘的HR朋友吃饭,她说自己周一到公司,光是处理三个岗位的需求邮件、筛简历、约面试就花掉一整天,晚上回家还在回复员工关于年假政策的咨询。这不是她不努力,而是HR日常工作里有一大堆“高重复、低决策、文本…

作者头像 李华
网站建设 2026/10/17 14:21:24

索尼爱立信W550C行货固件刷机全攻略:驱动安装到系统验证

简介:这份资源面向仍在使用索尼爱立信W550C行货机型、希望自行修复白屏与频繁死机等系统故障的用户,提供一套完整的刷机固件与配套工具。压缩包共37个文件,约39.35MB,包含XS2.2与XS3.2两个版本的刷机工具、Win_USB_Driver_new刷机…

作者头像 李华
网站建设 2026/10/17 14:21:06

Harbor私有镜像仓库离线安装与配置:从原生Registry到团队化治理

很多人第一次搞私有镜像仓库,都觉得这事简单:docker run -p 5000:5000 registry,完事。可真到了团队里大伙儿一起用,或者要往生产环境推,问题就接踵而至——没有登录鉴权、没有用户管理、镜像说删就删、说不清谁在什么…

作者头像 李华
网站建设 2026/10/17 14:20:29

SFML图形绘制黑屏排查与渲染原理深度解析

1. 为什么“绘制图形”是SFML入门真正的分水岭很多人学SFML,一开始被窗口创建、事件循环这些基础操作吸引,觉得“哦,能弹个黑窗出来了”,就以为自己上道了。但真正卡住90%初学者的,从来不是怎么开窗,而是—…

作者头像 李华