写命令行工具的时候,最烦人的部分之一就是解析用户输入——一堆if-elif判断命令,再根据子命令分发逻辑,代码一长就变得像意大利面条。Python 3.10引入了match-case结构,但真正好用是在3.12版本完善之后,尤其是序列模式和映射模式的增强。这篇就用一个实际的小项目——命令行任务管理器——来完整展示怎么用match-case写出清晰、可扩展的命令解析逻辑。
一、match-case能做什么
如果你写过C或JavaScript的switch,match-case看着眼熟,但它远比switch强大。它不是简单比较值,而是做“模式匹配”——根据数据结构解构并匹配。可以从一个元组、列表、字典甚至自定义类里提取字段,同时进行判断。
举个简单对比。传统做法:
command = input("> ").strip().split()
if command[0] == 'add':
if len(command) > 1:
add_task(command[1])
else:
print("用法:add [任务描述]")
elif command[0] == 'list':
list_tasks()
# 继续堆砌...
用了match-case:
command = input("> ").strip().split()
match command:
case ['add', *description]:
add_task(' '.join(description))
case ['list']:
list_tasks()
case _:
print("未知命令")
可以看到,模式匹配直接描述了“期望的输入形状”,解析和提取一步完成,而且代码量小,可读性高。
二、项目结构:一个命令行任务管理器
我们要做的工具名叫“pytask”,功能如下:
- 添加任务:
pytask add 买咖啡 - 列出任务:
pytask list或pytask list --done(显示已完成) - 完成任务:
pytask done 1(按序号完成) - 删除任务:
pytask delete 1 - 持久化:数据存到本地JSON文件
- 状态流转:任务有
pending和done两个状态,用match-case做状态机处理
任务用一个Python数据类表示:
from dataclasses import dataclass, field
from typing import Literal
@dataclass
class Task:
description: str
status: Literal['pending', 'done'] = 'pending'
三、从头实现
完整代码结构如下,我会分块解释。
3.1 数据层:加载和保存任务
import json
from pathlib import Path
TASKS_FILE = Path.home() / ".pytask.json"
def load_tasks() -> list[Task]:
if not TASKS_FILE.exists():
return []
with open(TASKS_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
return [Task(**item) for item in data]
def save_tasks(tasks: list[Task]):
with open(TASKS_FILE, 'w', encoding='utf-8') as f:
json.dump([{'description': t.description, 'status': t.status} for t in tasks], f, ensure_ascii=False, indent=2)
3.2 命令处理:用match-case解析指令
核心就在这里。我们输入一行文字,按空格拆成列表,然后匹配模式:
def parse_command(raw_input: str, tasks: list[Task]) -> str:
parts = raw_input.strip().split()
if not parts:
return "请输入命令"
match parts:
case ['add' | 'a', *desc]:
description = ' '.join(desc).strip()
if not description:
return "错误:请提供任务描述"
tasks.append(Task(description=description))
save_tasks(tasks)
return f"已添加任务:{description}"
case ['list' | 'ls', '--done' | '-d']:
done_tasks = [t for t in tasks if t.status == 'done']
if not done_tasks:
return "没有已完成的任务"
lines = [f"[x] {t.description}" for t in done_tasks]
return "已完成任务:n" + 'n'.join(lines)
case ['list' | 'ls']:
if not tasks:
return "任务列表为空"
lines = []
for i, t in enumerate(tasks, 1):
mark = 'x' if t.status == 'done' else ' '
lines.append(f"{i}. [{mark}] {t.description}")
return 'n'.join(lines)
case ['done' | 'do', index]:
try:
idx = int(index) - 1
if idx = len(tasks):
return "错误:无效的任务序号"
task = tasks[idx]
if task.status == 'done':
return "任务已完成"
task.status = 'done'
save_tasks(tasks)
return f"任务已完成:{task.description}"
except ValueError:
return "错误:序号必须为数字"
case ['delete' | 'del', index]:
try:
idx = int(index) - 1
if idx = len(tasks):
return "错误:无效的任务序号"
removed = tasks.pop(idx)
save_tasks(tasks)
return f"已删除任务:{removed.description}"
except ValueError:
return "错误:序号必须为数字"
case ['help' | 'h' | '?']:
return help_text()
case _:
return f"未知命令 '{' '.join(parts)}'。输入 help 查看帮助。"
用到了几种模式:
['add' | 'a', *desc]:第一个元素是’add’或’a’(用|表示或),后面零或多个元素捕获到desc列表。完美处理了不带参数和带空格描述的情况。['list' | 'ls', '--done' | '-d']:精确匹配两个元素,第二个是’-d’或’–done’。['done' | 'do', index]:捕获第二个元素作为字符串,后面再转整数。
注意模式中的变量(如index)是与捕获位置绑定的,不是和后面的变量同名,这很干净。
四、加入任务状态机:用match来处理批量操作
除了命令解析,我们还可以用match-case做任务状态流转。将来可能加入“暂停”“暂停中”等状态。这里展示一个简单的状态切换模式:
def change_task_status(task: Task, new_status: str):
match (task.status, new_status):
case ('pending', 'done'):
task.status = 'done'
case ('done', 'pending'):
task.status = 'pending' # 可以重新打开
case ('done', 'done') | ('pending', 'pending'):
pass # 不改变
case _:
raise ValueError(f"不允许从 {task.status} 到 {new_status} 的转换")
这个函数虽然没有用在CLI里(我们直接修改了status),但它展示了match-case处理状态机的天然优势:输入是元组,模式直接解构并判断组合,比一堆if-elif清晰很多。
五、主循环和帮助
def help_text():
return """可用命令:
add|a <描述> 添加任务
list|ls 列出所有任务
list|ls --done|-d 列出已完成任务
done|do <序号> 标记任务为完成
delete|del <序号> 删除任务
help|h|? 显示帮助
quit|exit|q 退出"""
def main():
tasks = load_tasks()
print("pytask 任务管理器。输入 help 查看命令,输入 quit 退出。")
while True:
try:
raw = input("> ").strip()
if raw in ('quit', 'exit', 'q'):
print("再见")
break
result = parse_command(raw, tasks)
print(result)
except (EOFError, KeyboardInterrupt):
print("n再见")
break
except Exception as e:
print(f"错误: {e}")
if __name__ == '__main__':
main()
六、完整运行效果
启动程序后:
> add 买咖啡
已添加任务:买咖啡
> add 写周报
已添加任务:写周报
> list
1. [ ] 买咖啡
2. [ ] 写周报
> done 1
任务已完成:买咖啡
> list --done
[x] 买咖啡
> delete 2
已删除任务:写周报
> quit
再见
七、为什么这套写法比if-elif更好扩展
假设以后要加一个“编辑任务描述”的功能,用match-case只需要加一个模式:
case ['edit' | 'e', index, *new_desc]:
# 解析序号和新描述
...
如果需求是“add”后面可以带--priority high选项,也可以直接扩展模式:
case ['add', '--priority', priority, *desc]:
不需要动其他分支,新增的匹配逻辑完全独立,可读性远胜于一长串if-elif。模式匹配的代码更像是“声明式”描述你期望的数据形状,而不是一步步的过程指令。
八、match-case的局限性及注意点
虽然好用,但不要过度使用。适用场景主要是:
- 根据数据结构(元组、列表、字典)的不同形状执行不同分支。
- 解构嵌套数据并同时检查值。
- 状态机或可数、可预定义的状态组合。
不适用场景:需要复杂的条件逻辑(如x > 5),那种情况还是用if语句。match-case的守卫子句if可以加,但不建议滥用,否则又变回传统写法。
另外,Python 3.10支持match-case,但3.12修复了一些边缘情况和提升了序列匹配的性能,所以建议在3.12以上的环境使用。
九、总结
用Python 3.12的match-case重构CLI工具之后,代码行数减少了约30%,而且新功能接入更快。核心思路是:把用户输入提炼成一个列表或元组,然后用模式匹配描述各种有效命令格式。这不仅让代码更紧凑,更重要的是让命令的定义“文档化”在代码结构里——扫一眼case块就知道系统支持哪些操作和它们对应的参数形状。
完整代码不到150行,可以直接保存为一个.py文件试用。如果你手头有正在维护的脚本工具,不妨抽一个模块用match-case重写试试,应该会立刻感受到模式匹配的爽感。

