当前位置: 首页 > news >正文

Python argparse 实战:用子命令、互斥参数和类型校验写一个像样的 CLI

Python argparse 实战:用子命令、互斥参数和类型校验写一个像样的 CLI

写脚本时你多半这么读命令行参数:sys.argv[1]拿第一个,sys.argv[2]拿第二个。脚本小的时候没问题,一旦参数多起来、有可选项、有默认值,这套就崩了——参数顺序一错全乱,少传一个直接IndexError,想加个--help还得自己拼字符串。

标准库的argparse就是干这个的。但很多人只会用它的皮毛(add_argument加几个位置参数),真正好用的子命令、互斥组、类型转换、自定义校验反而没碰过。这篇我们从一个实际需求出发,把它写成一个像样的命令行工具。

需求:一个文件处理 CLI

假设我们要做个工具filetool,支持两个子命令:

  • filetool compress <path> --level 9—— 压缩文件
  • filetool convert <path> --to png --quality 80—— 格式转换

先看没有 argparse 会写成什么样:

importsys# 朴素写法:脆弱、难维护cmd=sys.argv[1]path=sys.argv[2]ifcmd=='compress':level=int(sys.argv[3])iflen(sys.argv)>3else6# ...

参数一多,这里的sys.argv[3]会变成灾难:用户不按顺序传就错位,int()转换失败直接崩,没有任何友好提示。

第一步:基础 parser 与类型校验

importargparse parser=argparse.ArgumentParser(prog='filetool',description='一个文件压缩与转换工具',)parser.add_argument('path',help='要处理的文件路径',)parser.add_argument('--level',type=int,# argparse 自动转 int,转不了会报友好错误default=6,choices=range(1,10),# 限定 1-9,超范围自动拒绝help='压缩级别 1-9(默认 6)',)args=parser.parse_args()print(args.path,args.level)

type=int让 argparse 自己做转换,用户传--level abc会得到error: argument --level: invalid int value: 'abc',而不是一个丑陋的 traceback。choices直接把合法值锁死,省了你手写if not 1 <= level <= 9

第二步:自定义校验——type 可以是任意函数

type不只能填intfloat,它接受任何「接收字符串、返回目标值」的可调用对象。想校验文件必须存在?写个函数塞进去:

importargparsefrompathlibimportPathdefexisting_file(s:str)->Path:p=Path(s)ifnotp.is_file():# 抛这个异常,argparse 会转成友好的命令行错误raiseargparse.ArgumentTypeError(f'文件不存在:{s}')returnp parser=argparse.ArgumentParser(prog='filetool')parser.add_argument('path',type=existing_file,help='要处理的文件')args=parser.parse_args()# args.path 此时已经是一个校验过的 Path 对象,不是 strprint(args.path.stat().st_size)

关键点:校验失败要抛argparse.ArgumentTypeError,而不是ValueError或直接sys.exit。只有这个异常 argparse 才会包装成filetool: error: argument path: 文件不存在: xxx这种统一格式。返回值会直接成为args.path,类型都帮你转好了。

第三步:互斥参数——两个开关不能同时出现

比如转换时,--quiet(静默)和--verbose(啰嗦)逻辑上互斥,用户不该两个都传。用add_mutually_exclusive_group:

group=parser.add_mutually_exclusive_group()group.add_argument('--quiet',action='store_true',help='静默模式')group.add_argument('--verbose',action='store_true',help='详细输出')

用户同时传--quiet --verbose,argparse 直接报错:argument --verbose: not allowed with argument --quiet。这种约束靠自己写if很容易漏,交给互斥组一劳永逸。

第四步:子命令——subparsers

这是argparse最被低估的能力。git commit/git push这种「一个主命令带多个子命令、每个子命令有自己的参数」的结构,靠add_subparsers实现:

importargparsefrompathlibimportPathdefexisting_file(s:str)->Path:p=Path(s)ifnotp.is_file():raiseargparse.ArgumentTypeError(f'文件不存在:{s}')returnp parser=argparse.ArgumentParser(prog='filetool')# dest='cmd' 让我们能从 args.cmd 读出用户选了哪个子命令subparsers=parser.add_subparsers(dest='cmd',required=True)# 子命令 1:compressp_compress=subparsers.add_parser('compress',help='压缩文件')p_compress.add_argument('path',type=existing_file)p_compress.add_argument('--level',type=int,default=6,choices=range(1,10))# 子命令 2:convertp_convert=subparsers.add_parser('convert',help='格式转换')p_convert.add_argument('path',type=existing_file)p_convert.add_argument('--to',required=True,choices=['png','jpg','webp'])p_convert.add_argument('--quality',type=int,default=80)args=parser.parse_args()

required=True保证用户必须选一个子命令,否则直接提示。注意每个子命令的参数是独立的:--level只属于compress,--to只属于convert,互不干扰。

第五步:用 set_defaults 把子命令绑到处理函数

拿到args.cmd后写一堆if args.cmd == 'compress'不够优雅。更干净的做法是给每个子命令绑一个处理函数:

defdo_compress(args):print(f'压缩{args.path},级别{args.level}')defdo_convert(args):print(f'把{args.path}转成{args.to},质量{args.quality}')# 绑定:每个子命令关联一个 funcp_compress.set_defaults(func=do_compress)p_convert.set_defaults(func=do_convert)args=parser.parse_args()# 一行分发,不用 if-else 链args.func(args)

set_defaults(func=...)把处理函数塞进args,最后args.func(args)一行完成分发。加新子命令时只需add_parser+ 写个函数 +set_defaults,主流程完全不用动——这就是可扩展的写法。

完整可运行示例

importargparsefrompathlibimportPathdefexisting_file(s:str)->Path:p=Path(s)ifnotp.is_file():raiseargparse.ArgumentTypeError(f'文件不存在:{s}')returnpdefdo_compress(args):print(f'压缩{args.path},级别{args.level}')defdo_convert(args):mode='静默'ifargs.quietelse'详细'print(f'把{args.path}转成{args.to},质量{args.quality},{mode}模式')defbuild_parser():parser=argparse.ArgumentParser(prog='filetool',description='文件工具')sub=parser.add_subparsers(dest='cmd',required=True)pc=sub.add_parser('compress',help='压缩文件')pc.add_argument('path',type=existing_file)pc.add_argument('--level',type=int,default=6,choices=range(1,10))pc.set_defaults(func=do_compress)pv=sub.add_parser('convert',help='格式转换')pv.add_argument('path',type=existing_file)pv.add_argument('--to',required=True,choices=['png','jpg','webp'])pv.add_argument('--quality',type=int,default=80)g=pv.add_mutually_exclusive_group()g.add_argument('--quiet',action='store_true')g.add_argument('--verbose',action='store_true')pv.set_defaults(func=do_convert)returnparserif__name__=='__main__':args=build_parser().parse_args()args.func(args)

跑一下:

$ python filetool.py convert ./a.png--towebp--quality90--verbose把 a.png 转成 webp,质量90,详细模式 $ python filetool.py--help# 自动生成的帮助$ python filetool.py convert--help# 子命令也有独立帮助

小结

  • 别再手撸sys.argv[n],argparse 帮你搞定顺序、默认值、--help和错误提示。
  • type=接受任意「字符串进、目标值出」的函数,校验失败抛argparse.ArgumentTypeError才能得到友好错误。
  • choices锁定合法值,add_mutually_exclusive_group声明互斥,把约束交给框架而不是手写 if。
  • 子命令用add_subparsers,每个子命令参数独立;set_defaults(func=...)+args.func(args)实现零 if-else 分发。

一句话记忆:argparse 的正确用法不是「解析参数」,而是「声明式地描述你的命令行长什么样」,解析、校验、帮助、分发它全包了。

http://www.jsqmd.com/news/1315526/

相关文章:

  • 郴州黄金回收怎么选?正规资质+透明计价避坑指南 - 小仙贝贝
  • Grove BlinkM智能RGB LED模块:从I2C通信到自定义脚本的完整开发指南
  • 2026年摩托车托运回家全攻略:怎么托运摩托车最省心?附避坑指南 - 快递物流资讯
  • UE5 FPS游戏开发:从零构建角色、武器与交互系统
  • C++依赖管理利器cppdep:从原理到实战,解决编译与架构难题
  • 如何快速部署pi-subagents:生产环境终极配置指南
  • 暑期学习打卡=第十九天
  • 北京婚约解除纠纷律所:订婚关系终止法律后果及权益保障指南 - 品牌深度评测
  • Mandarine-NEO性能优化指南:让老电脑也能流畅运行3DS游戏
  • 寂静猎人:顶级散户的交易哲学与实战守则
  • UartSBee V3.1:基于FT232RL的USB转串口调试工具深度解析与应用
  • 如何彻底告别动漫资源搜索烦恼:AnimeGarden一站式聚合平台终极指南
  • 北京私分国有资产罪历史遗留问题律所处理方案:如何界定政策边界 - 品牌深度评测
  • WebGazer.js深度解析:如何用普通摄像头实现网页眼动追踪的实战指南
  • Unity粒子特效优化指南:风暴与暴风雪效果的性能调优实战
  • AI智能柜十大品牌怎么选?2026最新选购指南+避坑技巧快收藏! - 匠言榜单
  • 武汉高分复读生冲刺名校|江夏襄五高端复读集训,突破分数上限 - 湖北找学校
  • GCC编译选项深度解析:从诊断优化到实战调试
  • UE5关卡蓝图核心指南:从事件分发到媒体播放的全局逻辑设计
  • PUMA:DOA估计模式的改进实现(Matlab代码实现)
  • DEVC++编译日志窗口空白?从原理到实操的完整修复指南
  • Real-ESRGAN x4plus Anime 6B架构深度解析与实战指南
  • 操作系统调度算法:从FCFS到多级反馈队列的权衡艺术
  • 还在用单步预测?ALA-BiTCN-BiGRU一键实现多步预测吸引审稿人!附Matlab代码
  • 软件加密实战:一机一码授权与Enigma Protector双架构保护详解
  • 奈雪礼品卡回收到底能值多少钱?这几个渠道别再踩坑了 - 沃卡回收
  • 【单片机毕设案例分享】基于 DS1302 掉电时钟存储的酒精监测终端开发 多按键交互的单片机酒精报警阈值自定义系统实现(020401)
  • 从安卓彩蛋到ADB高阶搞机:探索系统趣味与实用调试技巧
  • Dev-C++安装配置全指南:C++初学者首选IDE的完整使用教程
  • 计算机单片机毕设实战-基于 STM32/51 单片机的手动自动双模式坐姿防护补光系统开发 基于单片机的多传感器融合智能学习照明提醒装置设计(021301)