argparse进阶:参数类型、默认值、choices、互斥组、子命令
好,咱们接着聊argparse。上一章我们搭了个基本架子,能跑通,能传参。但说实话,那只能算入门。真正的批量调试场景,参数一多,各种奇奇怪怪的需求就来了。比如:某个参数必须是整数,另一个只能从几个选项里挑,还有的参数必须二选一——这就要用到argparse的进阶功能了。
我个人习惯把这些功能分成两大类:参数约束和参数组织。参数约束管的是“参数长什么样”,参数组织管的是“参数怎么搭配”。咱们一个一个来。
参数类型与类型转换
先看个最基础的。默认情况下,argparse拿到的参数全是字符串。就算你传一个--count 5,那个5也是字符串。我在项目里见过有人直接拿字符串去算总数,结果拼了个“555”,排查半天。
解决办法很简单,用type参数指定类型。
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--count', type=int, default=1)
parser.add_argument('--rate', type=float, default=0.5)
parser.add_argument('--verbose', type=bool, default=False)
args = parser.parse_args()
print(type(args.count)) # <class 'int'>
print(type(args.rate)) # <class 'float'>
这里有个坑,我踩过。type=bool并不像你想的那么好用。你传--verbose False,它不会转成False,因为任何非空字符串都被Python当成True。
小心bool类型陷阱
type=bool不会把"false"字符串变成False。要么用action='store_true',要么自己写一个转换函数。
那正确的做法呢?我个人推荐两种方式:
- 用
action='store_true'或action='store_false',开关式参数。 - 如果需要显式传布尔值,自己写个转换函数。
def str_to_bool(value):
if value.lower() in ('yes', 'true', '1', 'y'):
return True
elif value.lower() in ('no', 'false', '0', 'n'):
return False
raise argparse.ArgumentTypeError(f'Invalid boolean value: {value}')
parser.add_argument('--debug', type=str_to_bool, default=False)
默认值,不只是兜底
default参数看起来简单,就是“用户没传就用这个”。但你想想看,如果默认值选得不好,调试的时候会出大问题。
我曾经在批量调试场景里犯过一个错:给--output-dir设了个默认值./output。结果跑了一批任务,所有结果都混在一起。默认值不应该只是占位符,它应该体现你的“最佳实践”或者“安全选择”。
默认值设计原则
1. 优先使用None,让后续代码自己判断。
2. 如果必须设默认值,确保它在大多数场景下安全。
3. 用default=argparse.SUPPRESS可以完全隐藏未传参数。
对了,还有一个技巧。默认值可以是动态的,比如从环境变量读取。
import os
default_threads = int(os.environ.get('THREADS', '4'))
parser.add_argument('--threads', type=int, default=default_threads)
这样做的好处是:用户可以通过环境变量统一设置,命令行参数又可以单独覆盖。灵活度很高。
choices:把参数关进笼子
有些参数的用户只应该从有限选项里选。比如日志级别、模式选择、环境区分。这时候choices就派上用场了。
parser.add_argument('--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
default='INFO')
parser.add_argument('--mode', choices=['train', 'eval', 'infer'],
default='train')
用户如果传了个--log-level TRACE,argparse会直接报错退出,并列出合法选项。省去你写if校验的功夫。
我有个习惯:choices列表最好写在模块级别的常量里,方便复用和维护。
VALID_MODES = ['dev', 'staging', 'prod']
VALID_LOGGERS = ['json', 'plain', 'silent']
parser.add_argument('--env', choices=VALID_MODES, required=True)
parser.add_argument('--log-format', choices=VALID_LOGGERS, default='plain')
还有个隐藏功能:choices可以接收一个容器,比如字典的键。这样参数定义和业务逻辑绑定得更紧。
MODE_CONFIG = {
'fast': {'batch': 64, 'lr': 0.01},
'balanced': {'batch': 32, 'lr': 0.001},
'accurate': {'batch': 16, 'lr': 0.0001},
}
parser.add_argument('--preset', choices=list(MODE_CONFIG.keys()), default='balanced')
args = parser.parse_args()
config = MODE_CONFIG[args.preset]
互斥组:你只能选一个
你有没有遇到过这种需求:--optimize和--debug只能开一个,或者必须二选一?用add_mutually_exclusive_group就对了。
group = parser.add_mutually_exclusive_group()
group.add_argument('--optimize', action='store_true', help='启用优化')
group.add_argument('--debug', action='store_true', help='调试模式')
用户如果同时用了--optimize和--debug,会直接报错。干干净净。
我曾经在数据管道里用互斥组来控制数据源:
source = parser.add_mutually_exclusive_group(required=True)
source.add_argument('--csv', type=str, help='从CSV文件读取')
source.add_argument('--api', type=str, help='从API接口读取')
source.add_argument('--db', type=str, help='从数据库读取')
注意,我加了required=True,用户必须并且只能选一个。这在批量对接任务里特别好用,不同数据源对应不同处理逻辑,互斥组保证了逻辑不会打架。
子命令:一个脚本搞定所有
这是argparse最强大的进阶功能,没有之一。如果你写过git clone、git commit、git push这种组合命令,那你就明白子命令的价值了。
用add_subparsers可以创建子命令,每个子命令有自己的参数集。
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='command', help='可用命令')
# 子命令:run
run_parser = subparsers.add_parser('run', help='运行任务')
run_parser.add_argument('--job-name', required=True)
run_parser.add_argument('--timeout', type=int, default=3600)
# 子命令:list
list_parser = subparsers.add_parser('list', help='列出任务')
list_parser.add_argument('--status', choices=['all', 'running', 'done', 'failed'],
default='all')
# 子命令:cancel
cancel_parser = subparsers.add_parser('cancel', help='取消任务')
cancel_parser.add_argument('job_id', type=int)
args = parser.parse_args()
if args.command == 'run':
run_job(args.job_name, args.timeout)
elif args.command == 'list':
list_jobs(args.status)
elif args.command == 'cancel':
cancel_job(args.job_id)
运行效果类似这样:
python batch_tool.py run --job-name batch1 --timeout 7200
python batch_tool.py list --status running
python batch_tool.py cancel 1024
dest='command'表示解析后通过args.command获取用户到底选了什么子命令。我在做CI/CD批量任务调度时,整个脚本就是靠子命令搭起来的,每个子命令对应一个流程步骤,清晰又好维护。
子命令最佳实践
1. 每个子命令的help写清楚,用户敲-h就能看到完整帮助。
2. 全局参数(比如--verbose、--config)放在主parser上,子命令专门放各自特有的参数。
3. 当子命令超过5个,可以考虑拆成独立文件,用import导入。
一张图掌握进阶结构
为了帮你快速建立整体认知,我画了一张结构图。你看一眼就能明白argparse进阶功能的组织关系。
从图上你可以看到,argparse进阶功能分四条主线。参数类型约束管“数据形状”,默认值策略管“安全边界”,choices管“选项范围”,互斥组和子命令管“参数之间的关系”。掌握了这四条线,你就能写出既健壮又灵活的CLI脚本。
组合起来看效果
最后,我给一个综合示例。假设我们做批量数据对接,需要支持多个数据源、多种格式、可调试、可优化:
parser = argparse.ArgumentParser(description='批量数据对接工具')
# 全局参数
parser.add_argument('--config', type=str, default='config.yaml')
parser.add_argument('--verbose', action='store_true')
# 互斥数据源
source = parser.add_mutually_exclusive_group(required=True)
source.add_argument('--api', type=str, help='API端点')
source.add_argument('--file', type=str, help='本地文件路径')
# choices约束参数
parser.add_argument('--format', choices=['csv', 'json', 'parquet'], default='csv')
parser.add_argument('--mode', choices=['sync', 'async'], default='sync')
# 类型限定
parser.add_argument('--retries', type=int, default=3)
parser.add_argument('--batch-size', type=int, default=100)
# 子命令
subparsers = parser.add_subparsers(dest='action', help='动作')
subparsers.add_parser('validate', help='校验配置')
subparsers.add_parser('dryrun', help='预运行')
subparsers.add_parser('execute', help='正式执行')
args = parser.parse_args()
这个脚本既可以做python tool.py --file data.csv execute,也可以python tool.py --api https://api.example.com validate。每一个命令组合都受约束保护,想出错都难。
好了,这一章的内容就这些。argparse进阶功能其实不复杂,关键在于组合运用。下次你写调试脚本的时候,可以试试把这些功能都用上,保证比你手写if判断清爽得多。