为什么argparse在指定可选参数的情况下包含默认值?

14

我正在使用Python 3.6的argparse库。 我使用可选参数来收集程序参数。 对于其中一些参数,我有合理的默认值,因此我将解析器配置为该参数的默认值。

In [2]: import argparse
   ...: import shlex
   ...: 
   ...: parser = argparse.ArgumentParser()
   ...: parser.add_argument('-s', '--samples', action='store', nargs='+', type=int, required=True,
   ...:                     help='number of samples')
   ...: parser.add_argument('-r', '--regions', action='append', nargs='+', type=str, default=['all'],
   ...:                     help='one or more region names. default to [\'all\']')

当未指定-r/--regions参数时,我希望看到配置的默认值(实际确实如此)。

In [3]: s = '-s 37'
   ...: parser.parse_args(shlex.split(s))
Out[3]: Namespace(regions=['all'], samples=[37])

当指定-r / --regions参数时,我期望只看到我提供给参数的值,但默认值也会显示出来。

In [5]: s = '-s 37 -r foo'
...: parser.parse_args(shlex.split(s))
Out[5]: Namespace(regions=['all', ['foo']], samples=[37])

这不是我预期的结果。我希望只有在没有提供可选参数时才会出现默认值。我查看了argparse代码并没有找到默认值被包含在哪里。根据注释,似乎逻辑是在处理实际提供的参数值之前将默认值添加到结果命名空间中之后。我原本希望反过来(即仅在到达args结尾并且没有看到具有默认值的参数时才应用默认值)。

是否有人能够解释一下这个问题?我是否错误地使用或理解了可选参数的默认选项的目的?是否有办法实现我想要的行为(即如果未提供可选值,则在命名空间中使用默认值)?

3个回答

11
处理默认值的逻辑是在解析开始时将所有默认值插入到命名空间中,然后让解析器替换它们。最后,在解析结束时有一个复杂的逻辑:
for each value in the namespace
   if it is a string and equals the default
      evaluate the string (with `type`) and put it back

对于普通的 store 操作,这很好用,并且让你可以提供字符串或任何你选择的默认值。

使用 append 就会产生意外的值。它将 ['all'] 放在命名空间中,然后将新值附加到其中。由于你的 nargs 是 '+', 它附加了一个列表,导致了字符串和列表的混合。

append 操作无法判断它是否将新值附加到了由 default 提供的列表还是多次先前的 appends 的结果列表中。如果默认值为 None,它将创建一个空列表,并将其附加到其中。

虽然这不符合你的预期,但实际上它给了你很多控制权。

最简单的解决方法是将默认值留为空。在解析后,只需检查该属性是否为 None,如果是,则用你的 ['all'] 替换它。这并不是邪恶或与 argparse 开发人员的设计意图相违背。毕竟,在解析所有输入之后,有些事情变得更容易。

这个问题已经在 Python bug/issues 上提出了,http://bugs.python.org/issue16399,可能在这里也提出过。但我认为修补程序最好能够向文档添加注释,类似于 optparse 中的这个:

"append 操作调用选项当前值的 append 方法。这意味着任何指定的默认值必须具有 append 方法。这也意味着如果默认值不为空,则命令行附加的任何值都将存在于该选项的解析值中,并在这些默认值之后附加。"

请参阅 bug/issues 以获取有关编写自己的 append Action 子类的想法。


感谢您解释逻辑和行为。我曾考虑在解析后测试None,然后应用默认值,但认为那是不好的风格。现在我明白了这是首选方法,我会直接这样做。 - Dig_Doug

3

您说的没错,使用append操作和非空默认值,任何提供的值都将附加到默认值而不是替换它 - 这是append的预期行为。

对于追加操作,更合适的代码如下:

parser.add_argument('-r', '--regions', action='append', type=str)
parser.parse_args('-r foo -r foo2'.split())
Namespace(regions=['foo', 'foo2'])

你会注意到在原始代码中,使用 nargs='+' 时,结果的 regions 值是一个嵌套的列表。由于 Append 操作已经将变量转换为列表,所以不需要使用 nargs。
要提供一个被解析器覆盖的默认值,请将默认值放在解析器名称空间之外,例如:
_DEFAULT_REGIONS = ['all']

parser = argparse.ArgumentParser()
parser.add_argument('-r', '--regions', action='append', type=str,
                    help="Defaults to %s" % (_DEFAULT_REGIONS))
parser.parse_args(<..>)

regions = parser.regions \
    if parser.regions is not None else _DEFAULT_REGIONS
function_using_regions(regions)

如果提供了parser.regions,则使用该值,否则使用_DEFAULT_REGIONS


0

你只需要移除 nargs 参数并使用 action=append

parser = argparse.ArgumentParser()
parser.add_argument('-i', default=[], action='append', type=str)
parser.parse_args('-i foo'.split())
# result >> Namespace(i=['foo'])

对于我的情况,我不想检查参数是否为None,然后再检查长度。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接