本节目标:用 argparse 把脚本变成带帮助、带校验、带子命令的命令行工具,并掌握退出码与 main 守卫的组织方式。
适用版本:Python 3.12+(实测 3.14.6)
11.3 argparse 与命令行工具
前两节我们把「时间」和「日志」这两个运行时基础设施装好了。本节解决最后一个基础问题:程序怎么接收外部指令。命令行参数是脚本与用户、与 CI、与其他程序交互的第一接口,argparse 是标准库自带的解析方案。
11.3.1 sys.argv 的原始形态
Python 把命令行参数原样放进 sys.argv:一个字符串列表,sys.argv[0] 是脚本名,后面依次是参数。运行 python argv1.py --name Alice -v 3 extra:
import sys
print("程序名 sys.argv[0]:", sys.argv[0])
print("其余参数 sys.argv[1:]:", sys.argv[1:])
程序名 sys.argv[0]: argv1.py
其余参数 sys.argv[1:]: ['--name', 'Alice', '-v', '3', 'extra']
全是字符串,也没有任何结构——-v 3 和 -v3 在你眼里也许是一回事,sys.argv 分不出来。手动解析这些字符串很快就会失控,所以需要 argparse。
11.3.2 第一个 argparse 程序
argparse 的三步套路:建 ArgumentParser、用 add_argument 声明参数、调 parse_args() 拿到结果对象。
import argparse
parser = argparse.ArgumentParser(prog="greet", description="向指定的人打招呼")
parser.add_argument("name", help="要问候的名字") # 位置参数
parser.add_argument("-g", "--greeting", default="你好", help="问候语") # 可选参数
parser.add_argument("-n", "--times", type=int, default=1, help="重复次数")
args = parser.parse_args()
for _ in range(args.times):
print(f"{args.greeting}, {args.name}!")
不带前导 - 的是位置参数(positional),必填;带 -/-- 的是可选参数(optional),可选。 运行结果:
python greet.py 世界
python greet.py 世界 -g 早上好 -n 3
你好, 世界!
早上好, 世界!
早上好, 世界!
早上好, 世界!
argparse 还免费送你 -h/--help:它会自动列出 usage、位置参数与每个可选参数的说明(含 default 提示)。后面 11.3.8 节会看到完整效果。
11.3.3 type= 与自定义类型函数
type=int 让 argparse 在解析时就把字符串转成整数,转不了会直接报错。若校验逻辑更复杂,就传一个自定义函数:
import argparse
def positive_int(value: str) -> int:
n = int(value)
if n <= 0:
raise argparse.ArgumentTypeError(f"必须是正整数,收到 {value!r}")
return n
parser = argparse.ArgumentParser(prog="greet")
parser.add_argument("name")
parser.add_argument("-n", "--times", type=positive_int, default=1)
args = parser.parse_args()
print(f"{args.name} x {args.times}")
python positive.py 世界 -n 0
usage: greet [-h] [-n TIMES] name
greet: error: argument -n/--times: 必须是正整数,收到 '0'
函数里抛 argparse.ArgumentTypeError(不是 ValueError),argparse 就会把消息包装成标准错误提示,并以退出码 2 结束。
11.3.4 choices、default 与 required
choices=[...]:把取值限定在一个集合内,超出即报错。default=...:不给参数时用的默认值。required=True:把可选参数变成必填(位置参数本来就必填,不需要它)。
parser.add_argument("-l", "--lang", choices=["zh", "en", "ja"], default="zh")
python lang.py 世界 -l fr
usage: greet [-h] [-l {zh,en,ja}] name
greet: error: argument -l/--lang: invalid choice: 'fr' (choose from 'zh', 'en', 'ja')
11.3.5 nargs:变长参数
nargs 控制一个参数能吃几个值:
| 取值 | 含义 |
|---|---|
N(整数) | 恰好 N 个,组成列表 |
"?" | 0 或 1 个;配合 const 使用 |
"*" | 0 个或多个 |
"+" | 至少 1 个 |
import argparse
parser = argparse.ArgumentParser(prog="nargs-demo")
parser.add_argument("files", nargs="+", help="至少一个文件")
parser.add_argument("--out", nargs="?", const="out.txt", default=None)
parser.add_argument("--include", nargs="*", default=[])
args = parser.parse_args()
print("files =", args.files)
print("out =", args.out)
print("include =", args.include)
python nargs_demo.py a.txt b.txt
python nargs_demo.py a.txt --out --include x y z
files = ['a.txt', 'b.txt']
out = None
include = []
files = ['a.txt']
out = out.txt
include = ['x', 'y', 'z']
--out 用了 nargs="?" + const="out.txt":给了 --out 但不带值,就取 const;完全不给,才用 default(这里是 None)。--include x y z 则靠 "*" 收走了后面三个词。
11.3.6 action:开关、计数与追加
action 改变参数的行为,几个常用值:
| action | 效果 |
|---|---|
"store_true" | 出现即 True,不出现即 False(开关) |
"count" | 每出现一次加 1(-vvv → 3) |
"append" | 每次出现追加进列表(可重复) |
import argparse
parser = argparse.ArgumentParser(prog="action-demo")
parser.add_argument("-v", "--verbose", action="count", default=0)
parser.add_argument("--tag", action="append", default=[])
parser.add_argument("--dry-run", action="store_true")
python action_demo.py -vvv --tag red --tag blue --dry-run
verbose = 3
tag = ['red', 'blue']
dry_run = True
-vvv 被识别成三个 -v,verbose 累加到 3——这是很多工具(如 curl -v、ssh -vvv)调日志级别的标准做法。
11.3.7 子命令:add_subparsers
当一个工具要做多件事(git commit、git push……),就用子命令:
import argparse
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="todo", description="一个最小的待办清单工具。",
epilog="示例:todo add '写文档' --priority high",
formatter_class=argparse.RawDescriptionHelpFormatter)
sub = parser.add_subparsers(dest="command", required=True)
p_add = sub.add_parser("add", help="添加一条待办")
p_add.add_argument("text")
p_add.add_argument("--priority", choices=["low", "mid", "high"], default="mid")
p_done = sub.add_parser("done", help="标记完成")
p_done.add_argument("id", type=int)
return parser
args = build_parser().parse_args()
print("子命令:", args.command)
python todo.py add "写文档" --priority high
python todo.py done 7
python todo.py
子命令: add
子命令: done
usage: todo [-h] {add,done} ...
todo: error: the following arguments are required: command
required=True 保证用户必须给出子命令,否则报错退出。每个子命令都有自己的 -h,帮助信息是分层的。
11.3.8 FileType 与帮助文本
argparse.FileType 会在解析阶段直接打开文件,省掉手动 open:
import argparse
parser = argparse.ArgumentParser(prog="filetype-demo")
parser.add_argument("src", type=argparse.FileType("r", encoding="utf-8"))
args = parser.parse_args()
print("读到行数:", len(args.src.read().splitlines()))
args.src.close()
python filetype_demo.py sample.txt
python filetype_demo.py nope.txt
读到行数: 2
usage: filetype-demo [-h] src
filetype-demo: error: argument src: can't open 'nope.txt': [Errno 2] No such file or directory: 'nope.txt'
文件不存在时 argparse 会直接给出友好错误,而不是抛一个裸的 FileNotFoundError。至于帮助文本排版,默认的 HelpFormatter 会压扁 description 里的换行缩进;想保留原样(如放示例),用 formatter_class=argparse.RawDescriptionHelpFormatter(下面是 11.3.7 那个 todo.py --help):
usage: todo [-h] {add,done} ...
一个最小的待办清单工具。
positional arguments:
{add,done}
add 添加一条待办
done 标记完成
options:
-h, --help show this help message and exit
示例:todo add '写文档' --priority high
最后那行「示例:…」就是通过 epilog + RawDescriptionHelpFormatter 原样保留的。
11.3.9 parse_known_args 与退出码
有时你写的是包装脚本:自己只认一部分参数,剩下的要原样透传给下游程序。parse_known_args() 返回「认识的参数 + 剩余的原始列表」:
import argparse
parser = argparse.ArgumentParser(prog="wrapper")
parser.add_argument("--config", default="app.toml")
parser.add_argument("-v", action="store_true")
known, rest = parser.parse_known_args()
print("known.config =", known.config)
print("rest =", rest)
if rest:
parser.error(f"无法识别的参数: {' '.join(rest)}")
python wrapper.py --config prod.toml -v --unknown foo
known.config = prod.toml
rest = ['--unknown', 'foo']
usage: wrapper [-h] [--config CONFIG] [-v]
wrapper: error: 无法识别的参数: --unknown foo
parser.error(msg) 会把消息打到 stderr 并以退出码 2 结束;-h 和解析错误也都是这个码。约定俗成的退出码是:0 成功、1 一般错误、2 用法错误。
11.3.10 main 守卫与组织方式
把逻辑收进 main(),用 if __name__ == "__main__" 守卫入口,是命令行工具的标准结构:
import argparse
import sys
def main(argv=None) -> int:
parser = argparse.ArgumentParser(prog="mypkg")
parser.add_argument("--fail", action="store_true")
args = parser.parse_args(argv)
if args.fail:
print("主动失败", file=sys.stderr)
return 1
print("成功")
return 0
if __name__ == "__main__":
raise SystemExit(main())
三个要点:main(argv=None) 让 parse_args(argv) 可注入参数,测试时无需真的启动进程;main 返回整数退出码,用 raise SystemExit(main()) 把它变成进程退出码;守卫让文件既能当脚本跑,又能被 import 复用而不会意外执行。
把文件放进包并加 __main__.py,就能用 python -m 包名 运行:python -m mypkg 打印 成功 并返回退出码 0,加 --fail 则打印 主动失败 并返回退出码 1。
小结
sys.argv只是字符串列表;argparse用ArgumentParser+add_argument+parse_args提供解析、类型转换、校验与帮助。- 位置参数必填,可选参数带
-/--;type=做转换(可传自定义函数抛ArgumentTypeError),choices限定取值,required=True强制可选参数。 nargs管数量(?/*/+),action管行为(store_true/count/append),add_subparsers实现多级子命令。FileType自动开关文件,RawDescriptionHelpFormatter保留帮助排版,parse_known_args支持透传。- 退出码 0 成功 / 1 一般错误 / 2 用法错误;用
main(argv=None) -> int+if __name__ == "__main__"组织入口。
到这里,第 11 章「时间、日志与命令行」就完整了:11.1 管时间,11.2 管运行记录,11.3 管外部接口。下一章我们进入并发——Python 的 GIL 到底是什么、线程与进程各适合什么场景。想先看更完整的 CLI 框架选型(Click、Typer),可延伸阅读 Python 命令行应用 。
阅读导航:上一节:logging 与结构化日志 · 下一节:GIL 与线程/进程模型 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。