argparse 位置多项选择默认子集:无效选择

Posted

技术标签:

【中文标题】argparse 位置多项选择默认子集:无效选择【英文标题】:Argparse positional multiple choices default subset: Invalid Choice 【发布时间】:2021-12-13 11:38:07 【问题描述】:

我使用argparse 来获取使用choicesnargs='*' 的元素子集。如果没有引入任何元素,我想默认返回元素的一个子集,但我得到一个Invalid Choice 错误。

这是我的示例代码:

from argparse import ArgumentParser

parser = ArgumentParser()
parser.add_argument(
    "choices",
    nargs="*",
    type=int,
    choices=[1,2,3,4,5],
    default=[1,3,5]
)

if __name__ == "__main__":
    print(parser.parse_args().choices)

我得到的输出是:

$ ./thing.py 3
[3]
$ ./thing.py 1 2
[1, 2]
$ ./thing.py
usage: thing.py [-h] [1,2,3,4,5 ...]
thing.py: error: argument choices: invalid choice: [1, 3, 5] (choose from 1, 2, 3, 4, 5)

我可以将默认值设置为 int,但不能设置为列表。有什么解决方法吗?

编辑:这似乎只发生在位置参数上。如果在非位置参数中使用 nargs="+"required=False 不会有任何问题。

【问题讨论】:

我最终放弃了使用位置参数,它降低了 CLI 的可读性。我会留下这个问题,以防有人遇到同样的问题并想解决它。 这是一个非常有趣的问题,几乎感觉像是一个错误。我试图开始调查(通过源代码),但现在还没有充分的时间。据我所知,如果您实际传递参数,它们将按以下顺序逐个执行:首先根据type 进行转换,然后检查它们是否在choices 中,然后附加到列表中(因为nargs) .使用默认值的时候,好像是直接保存到Namespace,然后签入choices会失败... 因为* 满足一个空列表,所以它的default 得到特殊处理。检查_get_values 子方法。这包括选择测试,非字符串默认值通常会绕过。 @Tomerikoo @hpaulj 感谢您的回复。不确定我是否完全理解,但我确实在docs 中看到“如果默认值是字符串,则解析器会解析该值,就好像它是命令行参数一样。特别是,解析器应用任何类型转换参数(如果提供),然后在命名空间返回值上设置属性。否则,解析器将按原样使用该值“但是当我尝试这样做时(default="1")我仍然得到invalid choice: '1' (choose from 1, 2, 3, 4, 5) @Tomerikoo,我在答案中详细说明了这种对默认值的处理。 【参考方案1】:

详细说明我的cmets。

默认值的正常处理,正如@Tomerikoo 引用的那样,是通过typechoices 检查来处理字符串默认值。默认值在解析开始时放置在 args 命名空间中。最后,如果没有使用输入覆盖默认值,它们将有条件地转换。非字符串默认值保持原样,不检查 typechoices

但是nargs='*' 的位置不同。它“总是可见”,因为字符串的空列表(即没有)满足其nargs。使用通常的处理方式,这将覆盖default,导致Namespace(choices=[])

为了解决这个问题,_get_values 方法将[] 替换为default。这个新值通过choices 测试,但不通过type。这会导致 OP 遇到的行为。

下面引用_get_values的相关部分。第一个处理nargs='?',第二个'*',和第三个正常情况。 _get_value 进行 type 测试。 _check_value 执行 choices 测试。

def _get_values(self, action, arg_strings):
    ....

    # optional argument produces a default when not present
    if not arg_strings and action.nargs == OPTIONAL:
        if action.option_strings:
            value = action.const
        else:
            value = action.default
        if isinstance(value, str):
            value = self._get_value(action, value)
            self._check_value(action, value)

    # when nargs='*' on a positional, if there were no command-line
    # args, use the default if it is anything other than None
    elif (not arg_strings and action.nargs == ZERO_OR_MORE and
          not action.option_strings):
        if action.default is not None:
            value = action.default
        else:
            value = arg_strings
        self._check_value(action, value)

    # single argument or optional argument produces a single value
    elif len(arg_strings) == 1 and action.nargs in [None, OPTIONAL]:
        arg_string, = arg_strings
        value = self._get_value(action, arg_string)
        self._check_value(action, value)

我应该添加另一个块,用于“*”和非空用户输入:

    # all other types of nargs produce a list
    else:
        value = [self._get_value(action, v) for v in arg_strings]
        for v in value:
            self._check_value(action, v)

请注意,这里的单个字符串是typechecked,在第一种情况下,它是整个default,即checked

存在与此相关的错误/问题,但没有人有充分的理由对其进行更改。

【讨论】:

以上是关于argparse 位置多项选择默认子集:无效选择的主要内容,如果未能解决你的问题,请参考以下文章

多项选择 - 下拉菜单

argparse:如何允许带有 nargs="*" 和选项的空列表

html控件 select 多项选择怎么没得到所选择的值

如何使用选择性结构调用基于 argparse 的函数?

带有全选选项的 swift 3.0 多项选择

Argparse 无法识别参数