Python 从零开始 · 第 14 课:综合实战——命令行小工具
这是 Python 系列的收官之战。前面 13 课,每一课你都"学会了一个零件"。这一课,我们要把这些零件组装成一台能转的机器:一个真正能用的命令行小工具。
我们的目标:写一个 待办事项(Todo)管理器。它能把任务存到文件里,随时增、删、查,下次打开程序数据还在。
为什么选"待办"?因为它几乎用遍了前 13 课的所有知识点,代码又不至于太长,是最佳的"毕业作品"。
一、先想清楚:程序要能做什么?
动手写代码前,先想明白功能,这叫"需求分析"。我们的工具要支持 4 个命令:
| 命令 | 作用 |
|---|---|
add 买菜 | 添加一条待办 |
list | 列出所有待办 |
done 1 | 把第 1 条标记为完成 |
del 1 | 删除第 1 条 |
数据存到一个文本文件里(比如 todo.txt),这样关掉程序数据也不丢——这用第 7 课的文件读写。
二、数据怎么存?——第 7 课:文件读写
最简单的存法:一行一条任务。完成了就写个 [x],没完成写 [ ]:
[x] 买菜
[ ] 写作业
[ ] 健身读文件用 with open(...) as f(第 7 课),把每一行读进一个列表:
def 读取任务():
try:
with open("todo.txt", "r", encoding="utf-8") as f:
return [line.strip() for line in f if line.strip()]
except FileNotFoundError:
return [] # 文件还不存在,就当没有任务这里用了第 9 课的try/except:第一次运行没有todo.txt,不能让它崩。
写回文件也简单:
def 保存任务(任务列表):
with open("todo.txt", "w", encoding="utf-8") as f:
for 任务 in 任务列表:
f.write(任务 + "\n")三、命令行参数怎么拿?—— sys.argv
程序要在命令行里接收 add 买菜 这样的输入,靠的是 sys.argv:一个列表,存着"运行命令时的所有参数"。
import sys
print(sys.argv)
# 运行 python todo.py add 买菜
# 输出:['todo.py', 'add', '买菜']sys.argv[0]:脚本名sys.argv[1]:第一个参数(这里是add)sys.argv[2]:第二个参数(这里是买菜)
所以我们的程序主体就是:看 sys.argv[1] 是什么命令,就干什么——这用第 3 课的 if/elif。
四、完整代码:全部串起来
新建一个 todo.py,把下面整段贴进去:
import sys
文件名 = "todo.txt"
def 读取任务():
"""读文件,返回任务列表。文件不存在就返回空列表。"""
try:
with open(文件名, "r", encoding="utf-8") as f:
return [line.strip() for line in f if line.strip()]
except FileNotFoundError:
return []
def 保存任务(任务列表):
"""把任务列表写回文件。"""
with open(文件名, "w", encoding="utf-8") as f:
for 任务 in 任务列表:
f.write(任务 + "\n")
def 添加(内容):
任务列表 = 读取任务()
任务列表.append(f"[ ] {内容}") # 新增,默认未完成
保存任务(任务列表)
print(f"✓ 已添加:{内容}")
def 列出():
任务列表 = 读取任务()
if not 任务列表:
print("暂无任务,先 add 一个吧~")
return
for i, 任务 in enumerate(任务列表, start=1):
print(f"{i}. {任务}") # 打印序号 + 任务
def 完成(序号):
任务列表 = 读取任务()
if 1 <= 序号 <= len(任务列表):
# 把 [ ] 换成 [x]
任务列表[序号 - 1] = 任务列表[序号 - 1].replace("[ ]", "[x]", 1)
保存任务(任务列表)
print(f"✓ 已完成:{任务列表[序号 - 1]}")
else:
print("✗ 序号超出范围,先 list 看看有几个任务")
def 删除(序号):
任务列表 = 读取任务()
if 1 <= 序号 <= len(任务列表):
被删 = 任务列表.pop(序号 - 1) # 第 4 课的 pop
保存任务(任务列表)
print(f"✓ 已删除:{被删}")
else:
print("✗ 序号超出范围,先 list 看看有几个任务")
def main():
if len(sys.argv) < 2: # 没给命令,提示用法
print("用法:")
print(" python todo.py add 内容 # 添加")
print(" python todo.py list # 列出")
print(" python todo.py done 序号 # 完成")
print(" python todo.py del 序号 # 删除")
return
命令 = sys.argv[1]
if 命令 == "add":
if len(sys.argv) >= 3:
添加(sys.argv[2])
else:
print("✗ add 后面要跟内容,如:python todo.py add 买菜")
elif 命令 == "list":
列出()
elif 命令 in ("done", "del"):
if len(sys.argv) >= 3 and sys.argv[2].isdigit():
序号 = int(sys.argv[2])
if 命令 == "done":
完成(序号)
else:
删除(序号)
else:
print("✗ 后面要跟数字序号,如:python todo.py done 1")
else:
print(f"✗ 未知命令:{命令},用 list 看看能干什么")
if __name__ == "__main__":
main()五、运行你的第一个小工具
在命令行里,切到 todo.py 所在目录,依次试:
python todo.py add 买菜
python todo.py add 写作业
python todo.py list
python todo.py done 1
python todo.py list
python todo.py del 2
python todo.py list你会看到任务一条条增加、完成、删除,而且关掉程序再打开,数据还在——因为它们已经存进了 todo.txt。
六、这份代码用到了哪些课?
复盘一下,你会发现这 100 行代码几乎串起了整个系列:
| 知识点 | 出处 | 用在哪 |
|---|---|---|
| 变量、字符串、列表 | 第 2、4 课 | 存任务、格式化 |
if/elif/else | 第 3 课 | 根据命令分流 |
for 循环、enumerate | 第 3 课 | 遍历打印任务 |
def 函数 | 第 6 课 | 每个功能一个函数 |
with open 文件读写 | 第 7 课 | 存/读任务 |
try/except | 第 9 课 | 处理文件不存在 |
str.replace、isdigit | 第 7 课 | 标记完成、校验输入 |
pop、切片 | 第 4 课 | 删除任务 |
七、进阶:argparse——命令行参数更专业
第三节用 sys.argv 手写参数判断,能跑,但有几个毛病:参数顺序固定、没有 --help、错误提示要自己一行行写。标准库里的 argparse 就是干这个的专业工具——它自动生成帮助、自动校验参数。
import argparse
parser = argparse.ArgumentParser(description="待办事项管理器")
parser.add_argument("命令", choices=["add", "list", "done", "del"], help="要执行的操作")
parser.add_argument("参数", nargs="?", default="", help="add 的内容,或 done/del 的序号")
args = parser.parse_args()
print("你输入了:", args.命令, args.参数)几个要点:
add_argument("命令", ...):声明一个位置参数,第一个字符串是参数名。choices=["add", ...]:限定只能传这几个值,传错自动报错并提示。nargs="?":这个参数可有可无;default=""给它一个默认值。parse_args()返回一个对象,用args.命令取参数值。
跑 python todo.py --help,argparse 会自动打印一份漂亮的用法说明——这是 sys.argv 手写做不到的。
把上一节 todo.py 的 main() 换成 argparse 版:
def main():
parser = argparse.ArgumentParser(description="待办事项管理器")
parser.add_argument("命令", choices=["add", "list", "done", "del"])
parser.add_argument("参数", nargs="?", default="")
args = parser.parse_args()
if args.命令 == "add":
if args.参数:
添加(args.参数)
else:
print("✗ add 后面要跟内容")
elif args.命令 == "list":
列出()
elif args.命令 == "done":
完成(int(args.参数))
elif args.命令 == "del":
删除(int(args.参数))
if __name__ == "__main__":
main()结论:练手用sys.argv理解原理,真写工具用argparse——参数校验、帮助信息都是白送的。
八、进阶:pytest——用测试守护你的代码
程序写大了,改一处经常带崩另一处。你怎么保证"这次改动没把原来好的功能弄坏"?答案是写测试——用代码来验证代码。
Python 生态最流行的测试框架是 pytest:
pip install pytest写第一个测试
假设 todo.py 里有这样一个"标记完成"的函数:
# todo.py 里
def 标记完成(任务):
return 任务.replace("[ ]", "[x]", 1)新建 test_todo.py,写测试:
# test_todo.py
from todo import 标记完成
def test_标记完成():
assert 标记完成("[ ] 买菜") == "[x] 买菜" # 断言:结果应该长这样
def test_已经是完成的():
assert 标记完成("[x] 写作业") == "[x] 写作业" # 重复完成不报错跑测试
在命令行敲:
pytest全绿就说明功能正常:
test_todo.py .. [100%]assert 是测试的灵魂:你断言"结果应该是什么",不对就报错。测试函数必须用 test_ 开头,pytest 才会自动找到它。
好处
- 改代码不慌:每改完一次跑
pytest,绿了说明没改坏,红了立刻知道哪里出问题。 - 当文档看:测试写了函数"该怎么用",别人一看就懂。
- 逼你把代码写干净:不好测的代码,往往也是不好维护的代码。
这套思路就是"测试驱动"的雏形,也是工程级开发的标配——本系列后面那些 AI Agent 背后,同样是一堆测试在兜底。
九、动手时间 🎯
实验 1:跑通并"玩坏"它
先正常跑一遍 5 条命令,然后故意输入错误命令(比如 todo.py done 99、todo.py xxx),看程序怎么友好地提示而不崩溃——这就是健壮性。
实验 2:加一个 clear 命令
给工具加第 5 个命令:clear 清空所有任务。提示:把空列表 保存任务([]) 就行。
实验 3:改造成"记账本"
把任务换成"收入/支出"记录,加一个 total 命令算总余额。存格式改成 +100 工资、-30 午饭 这种,用字符串方法解析数字。
实验 4(挑战):加上日期
每条任务加个添加时间,用第 8 课的标准库 datetime:
from datetime import datetime
时间 = datetime.now().strftime("%Y-%m-%d %H:%M")
任务列表.append(f"[ ] {内容} ({时间})")十、结业:你已经是"会写脚本的人"了
恭喜!走到这里,你已经:
- ✅ 会写能跑的 Python 程序
- ✅ 会读文件、写文件、处理异常
- ✅ 会用 numpy/pandas 玩数据
- ✅ 会联网调 API
- ✅ 能从零写一个命令行小工具
这套能力,已经足够你无缝进入《从算法到人工智能》系列——下一站:算法、数据结构,然后是机器学习、Transformer,直到写出你自己的 AI Agent。
十一、小结
- 写脚本的套路:先想清楚要做什么,再拆成"读数据 → 处理 → 输出"。
- 命令行参数用
sys.argv,文件读写用open(),异常用 try/except 兜底。 - 你已经能从零写能跑的小工具了——这是通往 AI Agent 的入口。
从零到能写脚本,你做到了。 现在,去写点什么吧。🎉