《Python编程入门》11.3 argparse 与命令行工具

sys.argv 太原始,argparse 是标准库自带的命令行解析方案。本节覆盖位置与可选参数、type 与 choices、nargs、store_true/count/append、子命令、FileType、parse_known_args 与退出码,并给出 __main__ 守卫组织方式,每个示例都真实运行。

本节目标:用 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 与线程/进程模型 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「python」更多文章

  1. 《Python高级编程》目录
  2. 《Python高级编程》11.3 PEP 流程与版本迁移策略
  3. 《Python高级编程》11.2 嵌入式与自由线程运行时