【问题标题】:Is there a way to use python argparse with nargs='*', choices, AND default?有没有办法将 python argparse 与 nargs=\'*\'、选择和默认值一起使用?
【发布时间】:2022-08-19 02:53:55
【问题描述】:

我的用例是多个可选位置参数,取自一组受限的choicesdefault 值是一个包含其中两个选项的列表。由于向后兼容性问题,我无法更改界面。我还必须保持与 Python 3.4 的兼容性。

这是我的代码。您可以看到我希望我的默认值是choices 集中的两个值的列表。

parser = argparse.ArgumentParser()
parser.add_argument(\'tests\', nargs=\'*\', choices=[\'a\', \'b\', \'c\', \'d\'],
                    default=[\'a\', \'d\'])
args = parser.parse_args()
print(args.tests)

所有这些都是正确的:

$ ./test.py a
[\'a\']
$ ./test.py a d
[\'a\', \'d\']
$ ./test.py a e
usage: test.py [-h] [{a,b,c,d} ...]
test.py: error: argument tests: invalid choice: \'e\' (choose from \'a\', \'b\', \'c\', \'d\')

这是不正确的:

$ ./test.py
usage: test.py [-h] [{a,b,c,d} ...]
test.py: error: argument tests: invalid choice: [\'a\', \'d\'] (choose from \'a\', \'b\', \'c\', \'d\')

我发现了很多类似的问题,但没有一个可以解决这个特定的用例。我发现的最有希望的建议(在不同的上下文中)是编写一个自定义操作并使用它而不是choices

那并不理想。我希望有人能指出我错过的一个选项。

如果没有,这是我计划使用的解决方法:

parser.add_argument(\'tests\', nargs=\'*\',
                    choices=[\'a\', \'b\', \'c\', \'d\', \'default\'],
                    default=\'default\')

只要我保持向后兼容性,我就可以添加参数。

谢谢!


更新:我最终选择了自定义操作。我很抗拒,因为这感觉不像是一个需要自定义任何东西的用例。然而,它似乎或多或少是子类化argparse.Action 的预期用例,它使意图非常明确,并给出了我发现的最干净的面向用户的结果。

class TestsArgAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        all_tests = [\'a\', \'b\', \'c\', \'d\']
        default_tests = [\'a\', \'d\']

        if not values:
            setattr(namespace, self.dest, default_tests)
            return

        # If no argument is specified, the default gets passed as a
        # string \'default\' instead of as a list [\'default\']. Probably
        # a bug in argparse. The below gives us a list.
        if not isinstance(values, list):
            values = [values]

        tests = set(values)

        # If \'all\', is found, replace it with the tests it represents.
        # For reasons of compatibility, \'all\' does not actually include
        # one of the tests (let\'s call it \'e\'). So we can\'t just do
        # tests = all_tests.
        try:
            tests.remove(\'all\')
            tests.update(set(all_tests))
        except KeyError:
            pass

        # Same for \'default\'
        try:
            tests.remove(\'default\')
            tests.update(set(default_tests))
        except KeyError:
            pass

        setattr(namespace, self.dest, sorted(list(tests)))
  • 另一个最近的选项和* nargs(不同的默认值),但除此之外相同点 - stackoverflow.com/questions/73205632/…。鉴于choices 的处理方式,没有简单的方法可以完成这项工作。
  • 这是一个有趣的方法。 (似乎enumerate 是不必要的,因为i 未使用)。我可能最终会使用它,因为无论如何我都可能会覆盖使用消息。唯一的缺点是,如果有人多次指定一个参数,它会中断。在我的用例中,他们没有理由这样做,但我更愿意宽容。我可能可以使用* 而不是?,我认为使用覆盖会很好。

标签: python parsing arguments argparse python-3.4


【解决方案1】:

标记为不正确的行为是由于原始默认值['a', 'd'] 不在指定的choices 内(请参阅:relevant code as found in Python 3.4.10;此检查方法自Python 3.10.3 起实际上未更改)。我将从 Python argparse.py 源代码复制代码:

    def _check_value(self, action, value):
        # converted value must be one of the choices (if specified)
        if action.choices is not None and value not in action.choices:
            args = {'value': value,
                    'choices': ', '.join(map(repr, action.choices))}
            msg = _('invalid choice: %(value)r (choose from %(choices)s)')
            raise ArgumentError(action, msg % args)

当一个默认值被指定为一个列表时,整个值将被传递给_check_value 方法,因此它将失败(因为任何给定的列表都不会匹配另一个列表中的任何字符串)。您实际上可以通过在该方法中使用pdb 设置断点并通过逐行遍历值来验证这一点,或者使用以下代码测试和验证所述限制:

import argparse
DEFAULT = ['a', 'd']
parser = argparse.ArgumentParser()
parser.add_argument('tests', nargs='*', choices=['a', 'b', 'c', 'd', DEFAULT],
                    default=DEFAULT)
args = parser.parse_args()
print(args.tests)

然后运行python test.py

$ python test.py
['a', 'd']

这显然通过了,因为在choices 列表中存在相同的DEFAULT 值。

但是,调用 -h 或传递任何不受支持的值将导致:

$ python test.py z
usage: test.py [-h] [{a,b,c,d,['a', 'd']} ...]
test.py: error: argument tests: invalid choice: 'z' (choose from 'a', 'b', 'c', 'd', ['a', 'd'])
$ python test.py -h
usage: test.py [-h] [{a,b,c,d,['a', 'd']} ...]

positional arguments:
  {a,b,c,d,['a', 'd']}
...

根据用例,这可能是理想的,也可能不是理想的,因为如果不混淆,输出看起来很奇怪。如果这个输出是面向用户的,它可能并不理想,但如果这是为了维护一些不会泄露给用户的内部系统调用仿真,那么消息可能是不可见的,所以这可能是一个可以接受的解决方法。因此,如果生成的选择消息的清晰度至关重要(超过 99% 的典型用例),我不推荐这种方法。

但是,鉴于自定义操作被认为不理想,我将假设覆盖 ArgumentParser 类可能是一个可能的选择,并且鉴于 _check_value 在 3.4 和 3.10 之间没有变化,这可能代表了要 nip 的最低限度的附加代码排除不兼容的检查(根据问题使用指定的用例):

class ArgumentParser(argparse.ArgumentParser):
    def _check_value(self, action, value):
        if value is action.default:
            return
        return super()._check_value(action, value)

这将确保在使用不适合问题中概述的要求的默认实现之前,默认值被视为有效选择(如果该值是操作的默认值,则返回 None,否则返回默认检查);请注意,这会阻止对action.default 提供的有效内容进行更深入的检查(如果有必要,自定义 Action 类肯定是要走的路)。

不妨展示自定义类的示例用法(即复制/粘贴原始代码,删除 argparse. 以使用新的自定义类):

parser = ArgumentParser()
parser.add_argument('tests', nargs='*', choices=['a', 'b', 'c', 'd'],
                    default=['a', 'd'])
args = parser.parse_args()
print(args.tests)

用法:

$ python test.py
['a', 'd']
$ python test.py a z
usage: test.py [-h] [{a,b,c,d} ...]
test.py: error: argument tests: invalid choice: 'z' (choose from 'a', 'b', 'c', 'd')
$ python test.py -h
usage: test.py [-h] [{a,b,c,d} ...]

positional arguments:
  {a,b,c,d}

optional arguments:
  -h, --help  show this help message and exit

【讨论】:

  • 谢谢你的彻底回答。这实际上是面向用户的(尽管大多数用户都是为这个软件做出贡献的人——这是测试套件代码)。所以将default 列表添加到choices 会太麻烦。我会考虑覆盖ArgumentParser。我想如果我要做任何我无法在单行中完成的事情,那将是自定义操作。
  • 别客气。自定义Action类最安全;我给出的答案是为了突出显示argparse 模块不支持开箱即用的内容/位置/原因,并在问题中列出的限制条件下生成解决方法的最低限度代码。
  • 我最终选择了一个自定义的Action 类。如果您有兴趣,请参阅描述的更新。再次感谢!
猜你喜欢
  • 2018-06-29
  • 2023-01-22
  • 2011-05-10
  • 2021-07-29
  • 2012-01-27
  • 2022-01-25
  • 2021-10-02
  • 1970-01-01
  • 2017-11-01
相关资源
最近更新 更多