命令行参数解析:从sys.argv到argparse

写过脚本的人都知道,最头疼的事之一就是硬编码参数。路径写死了、IP写死了、端口号也写死了。换一台机器或者改一次环境,就得打开脚本改代码。我个人习惯是所有可变参数都从命令行传进来。今天就聊聊怎么在Python里把这件事做漂亮。

认识sys.argv:最原始的参数获取方式

sys.argv是Python解释器自动维护的一个列表。你在命令行敲的东西,会被按空格拆成字符串,放进这个列表里。以我自己为例,看看下面这个最简单的脚本:

import sys
print(f"脚本名:{sys.argv[0]}")
print(f"参数个数:{len(sys.argv) - 1}")
print(f"参数列表:{sys.argv[1:]}")

假设这个文件叫demo.py,你在终端里执行:

python demo.py --host 192.168.1.1 --port 8080

输出会是什么?

脚本名:demo.py
参数个数:4
参数列表:['--host', '192.168.1.1', '--port', '8080']

sys.argv[0]永远是脚本本身的路径。真正的参数从下标1开始。我在项目里遇到过一个坑:如果脚本路径包含空格,sys.argv[0]会包含那个路径,但跨平台行为略有差异。嗯,这里要注意。

⚠ 类型问题:sys.argv里所有元素都是字符串。端口号“8080”是字符串,不是整数。你如果要做数学运算或者比较,必须自己做类型转换。几乎每个月都能看到有人因为忘记int()而出bug。

sys.argv的用法极其简单,但功能也很有限。我刚开始写脚本时只用它,后来发现每次都要写if-else去解析参数,代码又臭又长。你想想看,如果需要5个参数,就得写5组if判断,还容易搞混位置。

argparse:帮你把参数解析变成一句话

argparse是Python标准库,专门用来解析命令行参数。我建议你用标准方式导入:

import argparse

它的核心思路是三步走:定义参数 → 解析参数 → 使用参数。先看一个极简版:

import argparse

parser = argparse.ArgumentParser(description='批量对接参数调试脚本')
parser.add_argument('--host', type=str, help='目标服务器IP')
parser.add_argument('--port', type=int, help='端口号')

args = parser.parse_args()

print(f"连接 {args.host}:{args.port}")

执行方式不变:

python demo.py --host 192.168.1.1 --port 8080

输出:

连接 192.168.1.1:8080

看到了吗?port自动变成整数了。argparse帮你处理了类型转换、参数校验,甚至帮你生成了帮助信息。执行下面这个命令试试:

python demo.py --help

输出:

usage: demo.py [-h] [--host HOST] [--port PORT]

批量对接参数调试脚本

options:
  -h, --help  显示帮助信息并退出
  --host HOST 目标服务器IP
  --port PORT 端口号

这是我个人很偏爱的一个特性:免费帮助文档。团队里其他人用你的脚本,记不住参数就执行--help,省了多少沟通成本。

💡 个人经验:我写任何需要接受外部输入参数的脚本,直接在项目目录里放一个run.sh或者run.bat,里面写好完整的命令行调用。然后让argparse自动生成帮助信息。这样新人上手最快。

位置参数 vs 可选参数:选对场景很重要

argparse里参数分两种:位置参数(也叫必填参数)和可选参数(也叫命名参数)。

位置参数:必须按顺序传,不加参数名

位置参数就是使用时不需要写--xxx,直接写值。根据位置决定含义。

import argparse

parser = argparse.ArgumentParser(description='文件处理工具')
parser.add_argument('input_file', type=str, help='输入文件路径')
parser.add_argument('output_file', type=str, help='输出文件路径')

args = parser.parse_args()
print(f"从 {args.input_file} 读,写到 {args.output_file}")

执行:

python file_tool.py /tmp/in.csv /tmp/out.csv

我见过有团队把所有参数都设计成位置参数。结果就是脚本里面有十几个位置参数,调用时稍微传错一个顺序,整个结果就变了。后来花了两天重构。我以前吃过亏,现在坚持:变量参数超过2个,优先用--xxx可选参数。

可选参数:显式命名,顺序自由

可选参数必须用--或-前缀指定。你可以定义短名和长名:

import argparse

parser = argparse.ArgumentParser(description='数据库备份脚本')
parser.add_argument('-H', '--host', type=str, required=True, help='数据库地址')
parser.add_argument('-P', '--port', type=int, default=5432, help='端口号,默认5432')
parser.add_argument('--verbose', action='store_true', help='启用详细日志')

args = parser.parse_args()

if args.verbose:
    print(f"连接 {args.host}:{args.port}")
else:
    print("开始备份...")

执行方式灵活多样:

python backup.py -H 10.0.0.50 -P 3306 --verbose
python backup.py --host 10.0.0.50 --verbose
python backup.py -H 10.0.0.50

注意action='store_true'这个用法:你把--verbose当成一个开关,不带参数。脚本里有它就算True,没有就是False。

我写脚本时配参数的习惯是:必填项用位置参数或required=True的可选参数,可选项给默认值。让用户少打几个字,但又不失灵活性。

🔑 核心对比
特性 位置参数 可选参数
使用时前缀 无,直接写值 --xxx或-xxx
顺序是否敏感 是,严格按定义顺序 否,可以任意顺序
是否可设置默认值 否,除非是nargs='?' 是,default参数
是否可设为可省略 默认必填 可设为required=False
典型适用场景 路径、文件、必须的标识 配置、开关、可选项

组合实战:一个完整的调参脚本

写一个批量对接参数调试的小工具,把上面讲的都用上:

import argparse
import json
import sys

def parse_args():
    parser = argparse.ArgumentParser(
        description='批量参数调试工具 v2.0',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='示例: python debug.py config.json --timeout 30 --log-level DEBUG'
    )

    # 位置参数:配置文件
    parser.add_argument('config_file', type=str,
                        help='JSON格式的配置文件路径')

    # 可选参数:连接相关
    parser.add_argument('--timeout', type=int, default=10,
                        help='连接超时时间,单位秒,默认10')
    parser.add_argument('--retry', type=int, default=3,
                        help='失败重试次数,默认3')

    # 可选参数:调试开关
    parser.add_argument('--log-level', type=str,
                        choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
                        default='INFO',
                        help='日志级别,默认为INFO')
    parser.add_argument('--dry-run', action='store_true',
                        help='仅验证参数,不执行实际对接')

    # 可选参数:输出控制
    parser.add_argument('--output', type=str,
                        help='结果输出文件,默认输出到终端')

    return parser.parse_args()

if __name__ == '__main__':
    args = parse_args()
    print(json.dumps(vars(args), indent=2, ensure_ascii=False))

调用和输出:

python debug.py test.json --timeout 30 --log-level DEBUG --dry-run
{
  "config_file": "test.json",
  "timeout": 30,
  "retry": 3,
  "log_level": "DEBUG",
  "dry_run": true,
  "output": null
}

你看,choices限制了日志级别的取值范围,action='store_true'让dry-run变成一个布尔开关,default缺省值让脚本可以用最少参数就跑起来。这些设计都是在真实项目中磨出来的。我建议你记住这个模式,以后每写一个新脚本都可以快速套用。

⚠ 避坑指南:我曾经见过一个项目,脚本里同时用了sys.argv和argparse,而且用sys.argv原始列表去解析,把argparse的返回值扔一边。这导致维护的人要同时理解两套逻辑。统一只用一种。既然是Python 3项目,统一用argparse就好。
Python命令行参数解析流程 Python 命令行参数解析路径 终端命令行输入 sys.argv 原始字符串列表 手动if-else解析 容易出错,难维护 argparse 推荐方案 Click / Typer 第三方库,可后续学习 argparse 配置 add_argument() parse_args() args.host / args.port ... 💡 核心路径:sys.argv → argparse → 结构化参数对象 支持位置参数和可选参数,自动类型转换,内置帮助信息

从上面的SVG图可以清楚地看到,命令行输入的原始字符串经过sys.argv进入Python程序,然后我们通过argparse把它拆解成结构化的对象。位置参数和可选参数只是add_argument()时的一行配置不同。本质上,它们都在同一个命名空间里。

最后分享一个我最近在批量对接项目里的经验:脚本参数设计时,先定好哪些参数是本次调试必有的(比如服务器地址、认证token)——用位置参数或required=True。哪些是可以给默认值的(比如超时时间、日志级别)——用可选参数+default。这样既保证脚本不会因为缺参数就报错,又给了用户足够的自定义空间。

实战中,我再提醒一个细节:参数名的风格要统一。用了--log-level就用kebab-case到底,别中间混个--targetIp又用驼峰。argparse解析后会自动把--log-level转成args.log_level(下划线形式)。保持这个规则,你的参数接口看起来就非常专业。