配套专栏:Python 全栈修炼之路 第 12 篇《阶段实战——命令行工具开发》
难度分布:⭐ → ⭐⭐ → ⭐⭐ → ⭐⭐⭐ → ⭐⭐⭐ → ⭐⭐⭐⭐
核心覆盖:
argparse、@dataclass、JSON 持久化、rich美化输出、模块化设计、单元测试
前言
第十二篇综合运用了 Python 基础阶段的所有知识:数据类型、函数、文件操作、异常处理、模块组织、面向对象等。本练习从零开始,通过 6 个由易到难的项目,帮助你掌握从简单列表管理到完整 CLI 工具开发的全部技能。
题目一:文本版任务管理器 ⭐
📌 题目描述
用纯 Python 实现一个最简单的任务管理器,数据保存在内存中(列表),支持增删查改:
tm=TaskManager()tm.add("完成Python项目",priority="high")tm.add("阅读技术文档",priority="medium")tm.add("回复邮件",priority="low")tm.list()# 输出:# 1. [高] 完成Python项目# 2. [中] 阅读技术文档# 3. [低] 回复邮件tm.done(1)tm.list()# 1. [中] 阅读技术文档# 2. [低] 回复邮件# 3. [高✓] 完成Python项目tm.delete(2)tm.list()# 1. [低] 回复邮件# 2. [高✓] 完成Python项目💡 编程思路
这道题考察基础数据结构(列表/字典)+ 函数封装 + 类设计:
- 任务用字典表示:
{"title": ..., "priority": ..., "done": False}。 - 任务列表:
self.tasks = [],通过enumerate()生成序号。 - 优先级映射:使用字典
{"high": "高", ...}实现中文显示。 - done/delete:找到任务后修改状态或从列表移除。
🖥️ 参考代码
fromtypingimportOptionalclassTaskManager:"""纯内存版任务管理器。"""PRIORITY_MAP={"high":"高","medium":"中","low":"低",}def__init__(self):self.tasks:list[dict]=[]defadd(self,title:str,priority:str="medium")->None:"""添加任务。"""ifprioritynotinself.PRIORITY_MAP:raiseValueError(f"无效优先级:{priority},可选值:{list(self.PRIORITY_MAP.keys())}")task={"title":title,"priority":priority,"done":False,}self.tasks.append(task)print(f"✓ 添加任务:{title}")defdone(self,index:int)->None:"""标记任务完成(index 从 1 开始)。"""ifnot1<=index<=len(self.tasks):raiseIndexError(f"任务编号{index}不存在")self.tasks[index-1]["done"]=Trueprint(f"✓ 完成任务:{self.tasks[index-1]['title']}")defdelete(self,index:int)->None:"""删除任务(index 从 1 开始)。"""ifnot1<=index<=len(self.tasks):raiseIndexError(f"任务编号{index}不存在")removed=self.tasks.pop(index-1)print(f"✓ 删除任务:{removed['title']}")deflist(self)->None:"""列出所有任务。"""ifnotself.tasks:print("暂无任务")returnfori,taskinenumerate(self.tasks,1):status="✓"iftask["done"]else""priority_label=self.PRIORITY_MAP[task["priority"]]priority_str=f"[{priority_label}{status}]"print(f"{i}.{priority_str}{task['title']}")deffind(self,keyword:str)->list[tuple[int,dict]]:"""搜索包含关键词的任务。"""results=[]fori,taskinenumerate(self.tasks,1):ifkeyword.lower()intask["title"].lower():results.append((i,task))returnresults# 测试if__name__=="__main__":tm=TaskManager()print("=== 添加任务 ===")tm.add("完成Python项目",priority="high")tm.add("阅读技术文档",priority="medium")tm.add("回复邮件",priority="low")print("\n=== 任务列表 ===")tm.list()print("\n=== 搜索 ===")foridx,taskintm.find("文档"):print(f" #{idx}:{task['title']}")print("\n=== 完成任务 ===")tm.done(1)tm.list()print("\n=== 删除任务 ===")tm.delete(2)tm.list()print("\n所有测试通过 ✓")🔗 关联知识点
| 知识点 | 说明 |
|---|---|
list.append() | 添加任务到列表 |
list.pop() | 删除指定位置元素 |
enumerate(..., 1) | 带序号的遍历 |
| 字典映射 | 优先级中英文转换 |
| 函数封装 | 类方法设计 |
题目二:命令行版任务管理器(argparse) ⭐⭐
📌 题目描述
使用argparse将任务管理器改造为真正的命令行工具:
$ python task_cli.pyadd"完成项目文档"--priorityhigh ✓ 任务已添加 $ python task_cli.py list1.[高]完成项目文档 $ python task_cli.pydone1✓ 任务已完成 $ python task_cli.py delete1--force✓ 任务已删除 $ python task_cli.py--helpusage: task_cli.py[-h]{add,list,done,delete}... 任务管理器 CLI...💡 编程思路
这道题考察argparse 子命令解析 + CLI 程序结构:
- 子命令模式:使用
add_subparsers()为每个操作创建子命令。 - 参数定义:每个子命令定义自己的参数(
add_parser+add_argument)。 - 命令分发:通过
getattr(self, f"cmd_{args.command}")动态调用对应方法。 - 数据存储:仍然用内存列表,但加上文件持久化(保存到 JSON)。
🖥️ 参考代码
"""命令行版任务管理器(使用 argparse)。"""importargparseimportjsonimportosfrompathlibimportPathfromtypingimportOptionalclassTaskItem:"""任务数据类(用普通类模拟 dataclass)。"""def__init__(self,title:str,priority:str="medium",done:bool=False):self.title=title self.priority=priority self.done=donedefto_dict(self)->dict:return{"title":self.title,"priority":self.priority,"done":self.done}@classmethoddeffrom_dict(cls,data:dict)->"TaskItem":returncls(data["title"],data.get("priority","medium"),data.get("done",False))classTaskCLI:"""任务管理 CLI。"""DATA_FILE=Path.home()/".taskcli"/"tasks.json"def__init__(self):self.tasks:list[TaskItem]=[]self._ensure_data_dir()self._load()def_ensure_data_dir(self):self.DATA_FILE.parent.mkdir(parents=True,exist_ok=True)def_load(self):ifself.DATA_FILE.exists():try:withopen(self.DATA_FILE,"r",encoding="utf-8")asf:data=json.load(f)self.tasks=[TaskItem.from_dict(t)fortindata]except(json.JSONDecodeError,KeyError):self.tasks=[]def_save(self):withopen(self.DATA_FILE,"w",encoding="utf-8")asf:json.dump([t.to_dict()fortinself.tasks],f,ensure_ascii=False,indent=2)# === 命令处理方法 ===defcmd_add(self,args):task=TaskItem(title=args.title,priority=args.priority)self.tasks.append(task)self._save()print(f"✓ 任务已添加(当前共{len(self.tasks)}个任务)")defcmd_list(self,args):ifnotself.tasks:print("暂无任务")returnfori,taskinenumerate(self.tasks,1):mark="✓"iftask.doneelse" "priority_map={"high":"高","medium":"中","low":"低"}pri=priority_map[task.priority]status=f"[{pri}]"ifnottask.doneelsef"[{pri}✓]"print(f"{i}.{status}{task.title}")# 统计done_count=sum(1fortinself.tasksift.done)print(f"\n共{len(self.tasks)}个任务,已完成{done_count}个")defcmd_done(self,args):try:idx=int(args.index)-1ifnot0<=idx<len(self.tasks):raiseIndexError self.tasks[idx].done=Trueself._save()print(f"✓ 任务已完成:{self.tasks[idx].title}")except(ValueError,IndexError):print(f"✗ 任务编号{args.index}不存在")defcmd_delete(self,args):ifnotargs.force:confirm=input(f"确认删除任务{args.index}?[y/N]: ")ifconfirm.lower()!="y":print("已取消")returntry:idx=int(args.index)-1removed=self.tasks.pop(idx)self._save()print(f"✓ 任务已删除:{removed.title}")except(ValueError,IndexError):print(f"✗ 任务编号{args.index}不存在")defcmd_clear(self,args):ifnotargs.force:confirm=input(f"确认清空所有{len(self.tasks)}个任务?[y/N]: ")ifconfirm.lower()!="y":print("已取消")returnself.tasks.clear()self._save()print("✓ 所有任务已清空")defrun(self,args=None):"""运行 CLI。"""parser=self._create_parser()parsed=parser.parse_args(args)ifnotparsed.command:parser.print_help()returncmd_method=getattr(self,f"cmd_{parsed.command}",None)ifcmd_method:cmd_method(parsed)else:print(f"✗ 未知命令:{parsed.command}")def_create_parser(self)->argparse.ArgumentParser:parser=argparse.ArgumentParser(prog="taskcli",description="任务管理器 CLI",formatter_class=argparse.RawDescriptionHelpFormatter,)sub=parser.add_subparsers(dest="command",help="可用命令")# addp_add=sub.add_parser("add",help="添加新任务")p_add.add_argument("title",help="任务标题")p_add.add_argument("-p","--priority",choices=["high","medium","low"],default="medium",help="优先级(默认 medium)")# listsub.add_parser("list",help="列出所有任务")# donep_done=sub.add_parser("done",help="标记任务完成")p_done.add_argument("index",help="任务编号")# deletep_del=sub.add_parser("delete",help="删除任务")p_del.add_argument("index",help="任务编号")p_del.add_argument("-f","--force",action="store_true",help="跳过确认")# clearp_clear=sub.add_parser("clear",help="清空所有任务")p_clear.add_argument("-f","--force",action="store_true",help="跳过确认")returnparserdefmain():cli=TaskCLI()cli.run()if__name__=="__main__":main()🔗 关联知识点
| 知识点 | 说明 |
|---|---|
argparse.ArgumentParser | 命令行参数解析器 |
add_subparsers() | 子命令模式 |
add_argument() | 定义参数(位置/可选) |
getattr(dynamic) | 动态方法调用 |
| JSON 文件持久化 | _save()/_load() |
题目三:任务模型(dataclass + Enum + TypeHint) ⭐⭐
📌 题目描述
使用@dataclass、Enum和类型提示重构任务模型:
fromdatetimeimportdate,timedelta# 创建任务task=Task(title="完成项目文档",priority=Priority.HIGH,due_date=date.today()+timedelta(days=7))print(task)# Task: 完成项目文档 [高] 剩余7天# 逾期检测task.due_date=date.today()-timedelta(days=1)print(task.is_overdue())# Trueprint(task)# Task: 完成项目文档 [高] 已逾期!# 数据序列化data=task.to_dict()print(data)# {'title': '完成项目文档', 'priority': 'HIGH', 'due_date': '...', ...}# 比较和哈希task2=Task(title="完成项目文档",priority=Priority.HIGH)print(task==task2)# True(相同标题+优先级视为相等)💡 编程思路
这道题考察@dataclass+Enum+@property+ 类型提示:
@dataclass:自动生成__init__、__repr__、__eq__等方法。field(default_factory=...):为列表等可变类型提供默认值。@property:将is_overdue等方法转为只读属性。Enum:用枚举类定义优先级,保证类型安全。__post_init__:在初始化后进行验证。
🖥️ 参考代码
fromdataclassesimportdataclass,fieldfromdatetimeimportdate,datetimefromenumimportEnum,autofromtypingimportOptionalimportuuidclassPriority(Enum):"""任务优先级枚举。"""LOW=auto()MEDIUM=auto()HIGH=auto()@propertydeflabel(self)->str:return{Priority.LOW:"低",Priority.MEDIUM:"中",Priority.HIGH:"高"}[self]@propertydefweight(self)->int:"""优先级权重(数值越大越重要)。"""return{Priority.LOW:1,Priority.MEDIUM:2,Priority.HIGH:3}[self]classStatus(Enum):"""任务状态枚举。"""TODO=auto()IN_PROGRESS=auto()DONE=auto()@propertydeflabel(self)->str:return{Status.TODO:"待办",Status.IN_PROGRESS:"进行中",Status.DONE:"已完成"}[self]@dataclassclassTask:"""任务数据类。"""title:strpriority:Priority=Priority.MEDIUM status:Status=Status.TODO description:str=""due_date:Optional[date]=Nonetags:list[str]=field(default_factory=list)# 自动生成字段id:str=field(default_factory=lambda:str(uuid.<