Python argparse位置参数和子命令

11

我正在使用argparse,并尝试混合使用子命令和位置参数,出现了以下问题。

以下代码可以正常运行:

import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()

parser.add_argument('positional')
subparsers.add_parser('subpositional')

parser.parse_args('subpositional positional'.split())
上述代码将args解析为Namespace(positional='positional'),但是当我将位置参数更改为nargs='?'时,代码如下:
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()

parser.add_argument('positional', nargs='?')
subparsers.add_parser('subpositional')

parser.parse_args('subpositional positional'.split())

它报错:

usage: [-h] {subpositional} ... [positional]
: error: unrecognized arguments: positional

这是为什么呢?


1
顺便提一下,这似乎是一个已知的错误(http://bugs.python.org/issue9340),并且已经在最近的Python版本中修复了。 - Ricardo Cárdenes
4个回答

9
起初我和jcollado想的一样,但是如果后续(顶级)位置参数有特定的nargs(nargs = None,nargs = integer),那么它会按照你的期望工作。当nargs为'?'或'*'时,它就会失败,并且有时候当它是'+'时也会失败。所以,我深入到代码中,找出了问题所在。
实际上问题就在于传入参数的分配方式。为了弄清楚谁得到了什么,调用parse_args会将参数总结为一个字符串,比如"AA"(针对你的情况,A表示位置参数,O表示可选参数)。然后根据你通过.add_argument和.add_subparsers方法向解析器添加的动作,生成一个与该总结字符串匹配的正则表达式模式。
对于你的例子来说,不管怎样,参数字符串最终都是"AA"。不同的是要匹配的模式(可以在argparse.py中的_get_nargs_pattern下看到可能的模式)。对于subpositional,最终模式是"(-*A[-AO]*)",这意味着允许一个参数后跟任意数量的选项或参数。对于位置参数而言,它取决于传递给nargs的值:
- None => '(-*A-*)' - 3 => '(-*A-*A-*A-*)'(每个预期参数都带有一个'-*A') - '?' => '(-*A?-*)' - '*' => '(-*[A-]*)' - '+' => '(-*A[A-]*)'
这些模式被追加在一起,对于nargs=None(也就是你的有效例子),你最终得到的是'(-*A[-AO]*)(-*A-*)',它匹配两个组['A', 'A']。这样,subpositional只会解析subpositional(正如你想要的那样),而positiona则会匹配它的动作。
对于 nargs='?',你最终得到的是'(-*A[-AO]*)(-*A?-*)'。第二组完全由可选模式组成,由于*是贪婪的,这意味着第一组可匹配字符串中的所有内容,导致识别出两个组['AA', '']。这意味着subpositional得到了两个参数,并最终失败了。
有趣的是,nargs='+'的模式为'(-*A[-AO]*)(-*A[A-]*)',只要您仅传递一个参数就可以工作。例如:subpositional a,因为在第二组中需要至少一个位置参数。同样,由于第一组是贪婪的,因此传递subpositional a b c d会得到['AAAA', 'A'],这不是您想要的结果。
总之:这很混乱。我想这应该被认为是一个错误,但如果将模式转换为非贪婪模式,则不确定影响会是什么...

1
请注意,当然,在所有顶级之后添加子解析器(正如jcollado和argparse文档建议的那样)将消除歧义并按预期工作! - Ricardo Cárdenes

6
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('positional', nargs='?')

subparsers = parser.add_subparsers()
subparsers.add_parser('subpositional')

print(parser.parse_args(['positional', 'subpositional']))
# -> Namespace(positional='positional')
print(parser.parse_args(['subpositional']))
# -> Namespace(positional=None)
parser.print_usage()
# -> usage: bpython [-h] [positional] {subpositional} ...

常规做法是命令前(左侧)的参数属于主程序,命令后(右侧)的参数属于命令。因此,positional 应该在命令 subpositional 之前。示例程序: gittwistd
另外,带有 narg =? 的参数可能应该是一个选项(--opt=value),而不是位置参数。

如果子解析器有位置参数怎么办?我们如何让 print(parser.parse_args(['subpositional', 'subparserarg'])) 打印出:# -> Namespace(positional=None)?这应该表示我们选择了子命令 'subpositional',并且参数 positional 是可选的。这可能吗? - jmlopez

5
我认为问题在于调用add_subparsers时,会在原始解析器中添加一个新参数,以传递子解析器的名称。
例如,使用以下代码:
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()

parser.add_argument('positional')                                             
subparsers.add_parser('subpositional')                                             

parser.parse_args()

您会获得以下帮助字符串:
usage: test.py [-h] {subpositional} ... positional

positional arguments:
  {subpositional}
  positional

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

请注意,subpositional会在positional之前显示。我认为你想要的是在子解析器名称之前有位置参数。因此,你可能需要在子解析器之前添加该参数:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('positional')

subparsers = parser.add_subparsers()
subparsers.add_parser('subpositional')

parser.parse_args()

使用此代码获取的帮助字符串为:
usage: test.py [-h] positional {subpositional} ...

positional arguments:
  positional
  {subpositional}

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

这种方式先将参数传递给主解析器,然后是子解析器的名称,最后是子解析器的参数(如果有的话)。


很不幸,它似乎无法工作。帮助看起来确实应该是这样的,但在实践中 - 它并没有改变解析过程。 - kcpr

0

在Python 3.5中仍然很混乱。

我建议子类化ArgumentParser以保留所有剩余的位置参数,并稍后处理它们:

import argparse

class myArgumentParser(argparse.ArgumentParser):
    def parse_args(self, args=None, namespace=None):
       args, argv = self.parse_known_args(args, namespace)
       args.remaining_positionnals = argv
       return args

parser = myArgumentParser()

options = parser.parse_args()

剩余的位置参数在列表 options.remaining_positionals 中。


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