argparse:如何允许多个值覆盖默认值?

12

这与有关允许多次指定参数的问题有一定的关联。

我想要能够像这样多次指定一个选项:

 tool --foo 1 --foo 2 --foo 3

也可以像这样:

 tool a b c

我也想同时支持两者:

 tool a b c --foo 1 --foo2 --foo 3

这个方法与以下情况兼容:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('foo', nargs='*', action='append')
parser.add_argument('--foo', nargs='*', dest='foo', action='append')
结果列表可以很容易地展开:
args = parser.parse_args('a b c --foo 1 --foo 2 --foo 3'.split())
args.foo = [el for elements in args.foo for el in elements]
返回结果:

产生:

>>> args
Namespace(foo=['a', 'b', 'c', '1', '2', '3'])

我该如何添加默认值,以便在用户指定一个参数时不会立即使用默认值?

如果只是在其中一个add_argument()调用中添加default=[['spam']],则默认值始终是结果的一部分。我无法让argparse自己在用户提供自己的参数时删除它。

我希望argparse本身已经提供了解决方案。


2
晚来一步:虽然我理解将所有内容保留在“argparse”中的动机,但对于大多数应用程序而言,简单的if len(foo) > 1: foo.pop(0)是否解决了问题? - hBy2Py
2
@hBy2Py:我理解你的观点(是的),但是我想将其保留在库内。我经常是一个理想主义者,超越“足够好”的点,只是为了好玩和学习。对于实际提出的代码,可能更适合 if len(foo) == 0: foo.append(default)(并在代码中不指定默认值)。否则可能会有奇怪的边缘情况(顺序不能保证,不知道?或其他什么原因产生的?)。有趣的是,在mgilson回答之前,我自己写了一个Action子类来执行您建议的操作。我删除它是因为被接受的解决方案对我很好,而且更加简洁。 - cfi
1个回答

8

我认为这是对其他答案的稍微改进,它依赖于自定义操作的self.default属性:

import argparse
import sys

class Extender(argparse.Action):
    def __call__(self,parser,namespace,values,option_strings=None):
        #Need None here incase `argparse.SUPPRESS` was supplied for `dest`
        dest = getattr(namespace,self.dest,None) 
        #print dest,self.default,values,option_strings
        if(not hasattr(dest,'extend') or dest == self.default):
            dest = []
            setattr(namespace,self.dest,dest)
            #if default isn't set to None, this method might be called
            # with the default as `values` for other arguements which
            # share this destination.
            parser.set_defaults(**{self.dest:None}) 

        try:
            dest.extend(values)
        except ValueError:
            dest.append(values)

        #another option:
        #if not isinstance(values,basestring):
        #    dest.extend(values)
        #else:
        #    dest.append(values) #It's a string.  Oops.

def new_parser(args):
    parser = argparse.ArgumentParser()
    parser.add_argument('foo', nargs='*',action=Extender)
    parser.add_argument('--foo', nargs='*', dest='foo', action=Extender)
    parser.set_defaults(foo = [['spam']])
    return parser.parse_args(args.split())

tests = {'a b c --foo 1 --foo 2 --foo 3':['a','b','c','1','2','3'],
         '':[['spam']],
         'a b c --foo 1 2 3':['a','b','c','1','2','3'],
         '--foo 1':['1'],
         'a':['a']}

for s,r in tests.items():
    print ( "parsing: {0}".format(s) )
    args = new_parser(s)
    if(args.foo != r):
        print ("ERROR")
        print (args.foo)
        print (r)
        sys.exit(1)
    print ( args )
    print ('*'*80)

还要注意我使用了 parser.set_defaults(...) 来设置 foo 属性的默认值。

@cfi -- 我相信如果命名空间中没有默认值,argparse会提供None作为默认值,尽管您始终可以使用dest = getattr(namespace,self.dest,None)。接下来,我检查extend以确保对象是一个list(或类似于列表的)。如果它是None(尚未通过默认值设置),那么我们需要创建一个列表(然后在try/except块中使用)。 - mgilson
啊,嗯。如果用户错误地指定了例如字符串默认值,则在处理第一个参数时它将被静默覆盖为空列表,而不是引发异常。 - cfi
@cfi -- 我稍微修改了一下。我在getattr中添加了None,因为argparse.SUPPRESS会导致你所描述的异常。 - mgilson
@cfi -- 不完全正确。在这种情况下,如果用户执行了 parser.set_defaults(foo = 'spam'),然后没有给出 'foo' 命令行参数,那么他们将得到 Namespace(foo=['s','p','a','m'])(你是正确的--这仍然不是理想的,但可以在 __call__ 中轻松检查)。例如,你可以使用 if not isinstance(values,basestring): dest.extend(values); else: dest.append(values) 来代替 try/except - mgilson
1
这是不好的行为:我使用了 traceback.print_stacktrace(),发现如果一个位置参数有默认值(即使使用了 parser.set_defaults()),并且用户仅使用可选项调用工具,则 argparse 会通过 consume_positionals 调度最终调用到我们的 __call__ 方法,并将默认值放入 values 中。无法区分用户参数的调用。删除解析器默认值是个好主意!比污染参数结果命名空间要好得多。您的解决方案仍然对在位置 add_argument() 中设置默认值敏感。没关系。 - cfi
显示剩余13条评论

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