Argparse 教程

作者:

Tshepang Mbambo

本教程旨在温和地介绍 argparse,它是 Python 标准库中推荐的命令行解析模块。

备注

标准库包含另外两个与命令行参数处理直接相关的库:低级 optparse 模块(它可能需要更多代码来为给定应用程序进行配置,但也允许应用程序请求 argparse 不支持的行为),以及非常低级 getopt(它专门用作 C 程序员可用的 getopt() 函数族的等效项)。虽然这两个模块都没有在本指南中直接介绍,但 argparse 中的许多核心概念最初都源自 optparse,因此本教程的某些方面也与 optparse 用户相关。

概念

让我们通过使用 ls 命令来展示本介绍性教程中将探讨的功能类型

$ ls
cpython  devguide  prog.py  pypy  rm-unused-function.patch
$ ls pypy
ctypes_configure  demo  dotviewer  include  lib_pypy  lib-python ...
$ ls -l
total 20
drwxr-xr-x 19 wena wena 4096 Feb 18 18:51 cpython
drwxr-xr-x  4 wena wena 4096 Feb  8 12:04 devguide
-rwxr-xr-x  1 wena wena  535 Feb 19 00:05 prog.py
drwxr-xr-x 14 wena wena 4096 Feb  7 00:59 pypy
-rw-r--r--  1 wena wena  741 Feb 18 01:01 rm-unused-function.patch
$ ls --help
Usage: ls [OPTION]... [FILE]...
List information about the FILEs (the current directory by default).
Sort entries alphabetically if none of -cftuvSUX nor --sort is specified.
...

我们可以从这四个命令中学到一些概念

  • 不带任何选项运行时,ls 命令非常有用。它默认为显示当前目录的内容。

  • 如果我们需要超出其默认提供的内容,我们会告诉它更多。在这种情况下,我们希望它显示不同的目录 pypy。我们所做的就是指定一个称为位置参数的参数。之所以这样命名,是因为程序应该仅根据其在命令行上出现的位置来知道如何处理该值。这个概念与 cp 这样的命令更相关,其最基本用法是 cp SRC DEST。第一个位置是 你想要复制什么, 第二个位置是 你想要复制到哪里

  • 现在,假设我们想要更改程序的行为。在我们的示例中,我们为每个文件显示更多信息,而不是仅仅显示文件名。在这种情况下,-l 被称为可选参数。

  • 这是一段帮助文本。它非常有用,因为你可以遇到一个你以前从未使用过的程序,并且只需阅读其帮助文本即可了解其工作原理。

基础知识

让我们从一个非常简单的例子开始,它几乎什么都不做

import argparse
parser = argparse.ArgumentParser()
parser.parse_args()

以下是运行代码的结果

$ python prog.py
$ python prog.py --help
usage: prog.py [-h]

options:
  -h, --help  show this help message and exit
$ python prog.py --verbose
usage: prog.py [-h]
prog.py: error: unrecognized arguments: --verbose
$ python prog.py foo
usage: prog.py [-h]
prog.py: error: unrecognized arguments: foo

以下是正在发生的事情

  • 不带任何选项运行脚本会导致标准输出上不显示任何内容。这没多大用处。

  • 第二个开始显示 argparse 模块的有用性。我们几乎什么都没做,但已经得到了一个很好的帮助消息。

  • --help 选项,也可以缩写为 -h,是我们免费获得的唯一选项(即无需指定)。指定其他任何内容都会导致错误。但即使如此,我们也会免费获得一个有用的用法消息。

介绍位置参数

一个例子

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("echo")
args = parser.parse_args()
print(args.echo)

并运行代码

$ python prog.py
usage: prog.py [-h] echo
prog.py: error: the following arguments are required: echo
$ python prog.py --help
usage: prog.py [-h] echo

positional arguments:
  echo

options:
  -h, --help  show this help message and exit
$ python prog.py foo
foo

以下是正在发生的事情

  • 我们添加了 add_argument() 方法,我们用它来指定程序愿意接受哪些命令行选项。在这种情况下,我将其命名为 echo 以使其与其功能一致。

  • 现在调用我们的程序需要我们指定一个选项。

  • parse_args() 方法实际上返回了指定选项的一些数据,在本例中是 echo

  • 这个变量是 argparse 免费执行的某种“魔法”(即无需指定该值存储在哪个变量中)。你还会注意到它的名称与方法 echo 给出的字符串参数匹配。

但是请注意,尽管帮助显示看起来很不错,但它目前没有它可能的那样有用。例如,我们看到我们将 echo 作为位置参数,但我们不知道它做什么,除了通过猜测或阅读源代码。所以,让我们让它更有用一点

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("echo", help="echo the string you use here")
args = parser.parse_args()
print(args.echo)

我们得到

$ python prog.py -h
usage: prog.py [-h] echo

positional arguments:
  echo        echo the string you use here

options:
  -h, --help  show this help message and exit

现在,做一些更有用的事情怎么样

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", help="display a square of a given number")
args = parser.parse_args()
print(args.square**2)

以下是运行代码的结果

$ python prog.py 4
Traceback (most recent call last):
  File "prog.py", line 5, in <module>
    print(args.square**2)
TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'

那进展不顺利。这是因为 argparse 将我们给它的选项视为字符串,除非我们另有说明。所以,让我们告诉 argparse 将该输入视为整数

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", help="display a square of a given number",
                    type=int)
args = parser.parse_args()
print(args.square**2)

以下是运行代码的结果

$ python prog.py 4
16
$ python prog.py four
usage: prog.py [-h] square
prog.py: error: argument square: invalid int value: 'four'

进展顺利。现在程序甚至在处理之前,会在遇到不合法输入时有用地退出。

介绍可选参数

到目前为止,我们一直在玩位置参数。让我们看看如何添加可选参数

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--verbosity", help="increase output verbosity")
args = parser.parse_args()
if args.verbosity:
    print("verbosity turned on")

和输出

$ python prog.py --verbosity 1
verbosity turned on
$ python prog.py
$ python prog.py --help
usage: prog.py [-h] [--verbosity VERBOSITY]

options:
  -h, --help            show this help message and exit
  --verbosity VERBOSITY
                        increase output verbosity
$ python prog.py --verbosity
usage: prog.py [-h] [--verbosity VERBOSITY]
prog.py: error: argument --verbosity: expected one argument

以下是正在发生的事情

  • 程序编写为:当指定 --verbosity 时显示内容,不指定时显示空。

  • 为了表明该选项实际上是可选的,在不使用它运行程序时不会出现错误。请注意,默认情况下,如果未使用可选参数,则相关变量(在本例中为 args.verbosity)将被赋值为 None,这就是它未能通过 if 语句的真值测试的原因。

  • 帮助消息有点不同。

  • 使用 --verbosity 选项时,还必须指定某个值,任何值。

上面的例子接受 --verbosity 的任意整数值,但对于我们简单的程序,实际上只有两个值有用,TrueFalse。让我们相应地修改代码

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--verbose", help="increase output verbosity",
                    action="store_true")
args = parser.parse_args()
if args.verbose:
    print("verbosity turned on")

和输出

$ python prog.py --verbose
verbosity turned on
$ python prog.py --verbose 1
usage: prog.py [-h] [--verbose]
prog.py: error: unrecognized arguments: 1
$ python prog.py --help
usage: prog.py [-h] [--verbose]

options:
  -h, --help  show this help message and exit
  --verbose   increase output verbosity

以下是正在发生的事情

  • 现在,该选项更像是一个标志,而不是需要值的东西。我们甚至更改了选项的名称以符合这个想法。请注意,我们现在指定了一个新关键字 action,并为其赋予值 "store_true"。这意味着,如果指定了该选项,则将值 True 赋给 args.verbose。不指定则表示 False

  • 当你指定一个值时,它会抱怨,这符合标志的真正含义。

  • 注意不同的帮助文本。

短选项

如果你熟悉命令行用法,你会注意到我还没有触及选项短版本的话题。这很简单

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-v", "--verbose", help="increase output verbosity",
                    action="store_true")
args = parser.parse_args()
if args.verbose:
    print("verbosity turned on")

以下是结果

$ python prog.py -v
verbosity turned on
$ python prog.py --help
usage: prog.py [-h] [-v]

options:
  -h, --help     show this help message and exit
  -v, --verbose  increase output verbosity

请注意,新的功能也反映在帮助文本中。

组合位置参数和可选参数

我们的程序复杂度不断增加

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display a square of a given number")
parser.add_argument("-v", "--verbose", action="store_true",
                    help="increase output verbosity")
args = parser.parse_args()
answer = args.square**2
if args.verbose:
    print(f"the square of {args.square} equals {answer}")
else:
    print(answer)

现在是输出

$ python prog.py
usage: prog.py [-h] [-v] square
prog.py: error: the following arguments are required: square
$ python prog.py 4
16
$ python prog.py 4 --verbose
the square of 4 equals 16
$ python prog.py --verbose 4
the square of 4 equals 16
  • 我们重新引入了一个位置参数,因此出现了抱怨。

  • 注意,顺序并不重要。

我们如何让这个程序恢复拥有多个详细级别,并实际使用它们的能力

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display a square of a given number")
parser.add_argument("-v", "--verbosity", type=int,
                    help="increase output verbosity")
args = parser.parse_args()
answer = args.square**2
if args.verbosity == 2:
    print(f"the square of {args.square} equals {answer}")
elif args.verbosity == 1:
    print(f"{args.square}^2 == {answer}")
else:
    print(answer)

和输出

$ python prog.py 4
16
$ python prog.py 4 -v
usage: prog.py [-h] [-v VERBOSITY] square
prog.py: error: argument -v/--verbosity: expected one argument
$ python prog.py 4 -v 1
4^2 == 16
$ python prog.py 4 -v 2
the square of 4 equals 16
$ python prog.py 4 -v 3
16

除了最后一个,这些都看起来不错,最后一个暴露了我们程序中的一个 bug。让我们通过限制 --verbosity 选项可以接受的值来修复它

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display a square of a given number")
parser.add_argument("-v", "--verbosity", type=int, choices=[0, 1, 2],
                    help="increase output verbosity")
args = parser.parse_args()
answer = args.square**2
if args.verbosity == 2:
    print(f"the square of {args.square} equals {answer}")
elif args.verbosity == 1:
    print(f"{args.square}^2 == {answer}")
else:
    print(answer)

和输出

$ python prog.py 4 -v 3
usage: prog.py [-h] [-v {0,1,2}] square
prog.py: error: argument -v/--verbosity: invalid choice: 3 (choose from 0, 1, 2)
$ python prog.py 4 -h
usage: prog.py [-h] [-v {0,1,2}] square

positional arguments:
  square                display a square of a given number

options:
  -h, --help            show this help message and exit
  -v, --verbosity {0,1,2}
                        increase output verbosity

请注意,此更改也反映在错误消息和帮助字符串中。

现在,让我们使用另一种方法来处理详细程度,这很常见。它也与 CPython 可执行文件处理其自身的详细程度参数的方式相匹配(查看 python --help 的输出)

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display the square of a given number")
parser.add_argument("-v", "--verbosity", action="count",
                    help="increase output verbosity")
args = parser.parse_args()
answer = args.square**2
if args.verbosity == 2:
    print(f"the square of {args.square} equals {answer}")
elif args.verbosity == 1:
    print(f"{args.square}^2 == {answer}")
else:
    print(answer)

我们引入了另一个操作“count”,用于计算特定选项出现的次数。

$ python prog.py 4
16
$ python prog.py 4 -v
4^2 == 16
$ python prog.py 4 -vv
the square of 4 equals 16
$ python prog.py 4 --verbosity --verbosity
the square of 4 equals 16
$ python prog.py 4 -v 1
usage: prog.py [-h] [-v] square
prog.py: error: unrecognized arguments: 1
$ python prog.py 4 -h
usage: prog.py [-h] [-v] square

positional arguments:
  square           display a square of a given number

options:
  -h, --help       show this help message and exit
  -v, --verbosity  increase output verbosity
$ python prog.py 4 -vvv
16
  • 是的,它现在更像是一个标志(类似于我们脚本的先前版本中的 action="store_true")。这应该解释抱怨。

  • 它的行为也类似于“store_true”操作。

  • 现在演示一下“count”操作的作用。你可能以前见过这种用法。

  • 如果你不指定 -v 标志,则该标志被视为具有 None 值。

  • 正如预期的那样,指定该标志的完整形式,我们应该得到相同的输出。

  • 可惜,我们的帮助输出对脚本获得的新功能信息量不大,但这始终可以通过改进脚本的文档(例如通过 help 关键字参数)来解决。

  • 最后一个输出暴露了我们程序中的一个 bug。

让我们修复

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display a square of a given number")
parser.add_argument("-v", "--verbosity", action="count",
                    help="increase output verbosity")
args = parser.parse_args()
answer = args.square**2

# bugfix: replace == with >=
if args.verbosity >= 2:
    print(f"the square of {args.square} equals {answer}")
elif args.verbosity >= 1:
    print(f"{args.square}^2 == {answer}")
else:
    print(answer)

结果如下

$ python prog.py 4 -vvv
the square of 4 equals 16
$ python prog.py 4 -vvvv
the square of 4 equals 16
$ python prog.py 4
Traceback (most recent call last):
  File "prog.py", line 11, in <module>
    if args.verbosity >= 2:
TypeError: '>=' not supported between instances of 'NoneType' and 'int'
  • 第一个输出进展顺利,并修复了我们之前的 bug。也就是说,我们希望任何 >= 2 的值都尽可能详细。

  • 第三次输出不太好。

让我们修复这个 bug

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("square", type=int,
                    help="display a square of a given number")
parser.add_argument("-v", "--verbosity", action="count", default=0,
                    help="increase output verbosity")
args = parser.parse_args()
answer = args.square**2
if args.verbosity >= 2:
    print(f"the square of {args.square} equals {answer}")
elif args.verbosity >= 1:
    print(f"{args.square}^2 == {answer}")
else:
    print(answer)

我们刚刚引入了另一个关键字 default。我们将其设置为 0 以便将其与其他整数值进行比较。请记住,默认情况下,如果未指定可选参数,则它会获得 None 值,并且无法将其与整数值进行比较(因此会引发 TypeError 异常)。

并且

$ python prog.py 4
16

仅凭我们目前所学,你可以走得很远,而我们只是触及了皮毛。argparse 模块非常强大,在本教程结束前,我们将进一步探索它。

更高级一点

如果我们想扩展我们的小程序以执行其他幂运算,而不仅仅是平方,该怎么办

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("x", type=int, help="the base")
parser.add_argument("y", type=int, help="the exponent")
parser.add_argument("-v", "--verbosity", action="count", default=0)
args = parser.parse_args()
answer = args.x**args.y
if args.verbosity >= 2:
    print(f"{args.x} to the power {args.y} equals {answer}")
elif args.verbosity >= 1:
    print(f"{args.x}^{args.y} == {answer}")
else:
    print(answer)

输出

$ python prog.py
usage: prog.py [-h] [-v] x y
prog.py: error: the following arguments are required: x, y
$ python prog.py -h
usage: prog.py [-h] [-v] x y

positional arguments:
  x                the base
  y                the exponent

options:
  -h, --help       show this help message and exit
  -v, --verbosity
$ python prog.py 4 2 -v
4^2 == 16

请注意,到目前为止,我们一直在使用详细级别来 更改 显示的文本。下面的示例反而使用详细级别来显示 更多 文本

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("x", type=int, help="the base")
parser.add_argument("y", type=int, help="the exponent")
parser.add_argument("-v", "--verbosity", action="count", default=0)
args = parser.parse_args()
answer = args.x**args.y
if args.verbosity >= 2:
    print(f"Running '{__file__}'")
if args.verbosity >= 1:
    print(f"{args.x}^{args.y} == ", end="")
print(answer)

输出

$ python prog.py 4 2
16
$ python prog.py 4 2 -v
4^2 == 16
$ python prog.py 4 2 -vv
Running 'prog.py'
4^2 == 16

指定不明确的参数

当在决定参数是位置参数还是某个参数时存在歧义时,可以使用 -- 来告诉 parse_args(),其后的所有内容都是位置参数

>>> parser = argparse.ArgumentParser(prog='PROG')
>>> parser.add_argument('-n', nargs='+')
>>> parser.add_argument('args', nargs='*')

>>> # ambiguous, so parse_args assumes it's an option
>>> parser.parse_args(['-f'])
usage: PROG [-h] [-n N [N ...]] [args ...]
PROG: error: unrecognized arguments: -f

>>> parser.parse_args(['--', '-f'])
Namespace(args=['-f'], n=None)

>>> # ambiguous, so the -n option greedily accepts arguments
>>> parser.parse_args(['-n', '1', '2', '3'])
Namespace(args=[], n=['1', '2', '3'])

>>> parser.parse_args(['-n', '1', '--', '2', '3'])
Namespace(args=['2', '3'], n=['1'])

冲突选项

到目前为止,我们一直在使用 argparse.ArgumentParser 实例的两种方法。让我们介绍第三种方法 add_mutually_exclusive_group()。它允许我们指定相互冲突的选项。我们还将更改程序的其余部分,以使新功能更有意义:我们将引入 --quiet 选项,它将与 --verbose 选项相反

import argparse

parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("-v", "--verbose", action="store_true")
group.add_argument("-q", "--quiet", action="store_true")
parser.add_argument("x", type=int, help="the base")
parser.add_argument("y", type=int, help="the exponent")
args = parser.parse_args()
answer = args.x**args.y

if args.quiet:
    print(answer)
elif args.verbose:
    print(f"{args.x} to the power {args.y} equals {answer}")
else:
    print(f"{args.x}^{args.y} == {answer}")

我们的程序现在更简单了,为了演示,我们失去了一些功能。无论如何,这是输出

$ python prog.py 4 2
4^2 == 16
$ python prog.py 4 2 -q
16
$ python prog.py 4 2 -v
4 to the power 2 equals 16
$ python prog.py 4 2 -vq
usage: prog.py [-h] [-v | -q] x y
prog.py: error: argument -q/--quiet: not allowed with argument -v/--verbose
$ python prog.py 4 2 -v --quiet
usage: prog.py [-h] [-v | -q] x y
prog.py: error: argument -q/--quiet: not allowed with argument -v/--verbose

这应该很容易理解。我添加了最后一个输出,这样你就可以看到你获得的灵活性,即长形式选项和短形式选项的混合使用。

在我们结束之前,你可能想告诉你的用户你程序的主要目的,以防他们不知道

import argparse

parser = argparse.ArgumentParser(description="calculate X to the power of Y")
group = parser.add_mutually_exclusive_group()
group.add_argument("-v", "--verbose", action="store_true")
group.add_argument("-q", "--quiet", action="store_true")
parser.add_argument("x", type=int, help="the base")
parser.add_argument("y", type=int, help="the exponent")
args = parser.parse_args()
answer = args.x**args.y

if args.quiet:
    print(answer)
elif args.verbose:
    print(f"{args.x} to the power {args.y} equals {answer}")
else:
    print(f"{args.x}^{args.y} == {answer}")

注意用法文本的细微差别。注意 [-v | -q],它告诉我们我们可以使用 -v-q,但不能同时使用两者

$ python prog.py --help
usage: prog.py [-h] [-v | -q] x y

calculate X to the power of Y

positional arguments:
  x              the base
  y              the exponent

options:
  -h, --help     show this help message and exit
  -v, --verbose
  -q, --quiet

如何翻译 argparse 输出

argparse 模块的输出,例如其帮助文本和错误消息,都可以使用 gettext 模块进行翻译。这允许应用程序轻松地本地化 argparse 生成的消息。另请参阅 国际化你的程序和模块

例如,在此 argparse 输出中

$ python prog.py --help
usage: prog.py [-h] [-v | -q] x y

calculate X to the power of Y

positional arguments:
  x              the base
  y              the exponent

options:
  -h, --help     show this help message and exit
  -v, --verbose
  -q, --quiet

字符串 usage:positional arguments:options:show this help message and exit 都是可翻译的。

为了翻译这些字符串,它们必须首先提取到 .po 文件中。例如,使用 Babel,运行此命令

$ pybabel extract -o messages.po /usr/lib/python3.12/argparse.py

此命令将从 argparse 模块中提取所有可翻译字符串,并将其输出到名为 messages.po 的文件中。此命令假设你的 Python 安装在 /usr/lib 中。

你可以使用此脚本找出系统上 argparse 模块的位置

import argparse
print(argparse.__file__)

一旦 .po 文件中的消息被翻译并且使用 gettext 安装了翻译,argparse 将能够显示翻译后的消息。

要在 argparse 输出中翻译你自己的字符串,请使用 gettext

自定义类型转换器

argparse 模块允许你为命令行参数指定自定义类型转换器。这允许你在用户输入存储到 argparse.Namespace 之前对其进行修改。当你需要在程序中使用输入之前对其进行预处理时,这会很有用。

使用自定义类型转换器时,你可以使用任何接受单个字符串参数(参数值)并返回转换后值的可调用对象。但是,如果你需要处理更复杂的场景,则可以使用带有 **action** 参数的自定义操作类。

例如,假设你想处理具有不同前缀的参数并相应地进行处理

import argparse

parser = argparse.ArgumentParser(prefix_chars='-+')

parser.add_argument('-a', metavar='<value>', action='append',
                    type=lambda x: ('-', x))
parser.add_argument('+a', metavar='<value>', action='append',
                    type=lambda x: ('+', x))

args = parser.parse_args()
print(args)

输出

$ python prog.py -a value1 +a value2
Namespace(a=[('-', 'value1'), ('+', 'value2')])

在此示例中,我们

  • 使用 prefix_chars 参数创建了一个具有自定义前缀字符的解析器。

  • 定义了两个参数,-a+a,它们使用 type 参数创建了自定义类型转换器,以将值存储在带有前缀的元组中。

如果没有自定义类型转换器,参数会将 -a+a 视为同一参数,这是不希望的。通过使用自定义类型转换器,我们能够区分这两个参数。

结论

argparse 模块提供了比此处展示的更多功能。它的文档非常详细和全面,并且充满了示例。通过本教程,你将能够轻松消化它们而不会感到不知所措。