argparse - 组合父解析器,子解析器和默认值

时间:2014-07-10 01:34:21

标签: python argparse

我想在脚本中定义不同的子分析器,同时继承来自公共父级的选项,但具有不同的默认值。但是,它并没有像预期的那样发挥作用。

这就是我的所作所为:

import argparse

# this is the top level parser
parser = argparse.ArgumentParser(description='bla bla')

# this serves as a parent parser
base_parser = argparse.ArgumentParser(add_help=False)
base_parser.add_argument('-n', help='number', type=int)


# subparsers
subparsers = parser.add_subparsers()
subparser1= subparsers.add_parser('a', help='subparser 1', 
                                   parents=[base_parser])
subparser1.set_defaults(n=50)
subparser2 = subparsers.add_parser('b', help='subparser 2',
                                   parents=[base_parser])
subparser2.set_defaults(n=20)

args = parser.parse_args()
print args

当我从命令行运行脚本时,这就是我得到的:

$ python subparse.py b
Namespace(n=20)

$ python subparse.py a
Namespace(n=20)

显然,第二个set_defaults会覆盖父级中的第一个args。由于argparse文档中没有任何相关内容(非常详细),我认为这可能是一个错误。

这有一些简单的解决方案吗?我之后可以检查None变量,并将{{1}}值替换为每个子分析符的预期默认值,但这是我期望argparse为我做的。

顺便说一句,这是Python 2.7。

3 个答案:

答案 0 :(得分:7)

set_defaults遍历解析器的操作,并设置每个default属性:

   def set_defaults(self, **kwargs):
        ...
        for action in self._actions:
            if action.dest in kwargs:
                action.default = kwargs[action.dest]

您定义-n时创建了action参数(base_parser对象)。使用parents创建每个子分析程序时,该操作将添加到每个子分析程序的._actions列表中。它没有定义新的行动;它只是复制指针。

因此,当您在set_defaults上使用subparser2时,您可以修改此共享操作的default

此操作可能是subparser1._action列表中的第2项(h是第一项)。

 subparser1._actions[1].dest  # 'n'
 subparser1._actions[1] is subparser2._actions[1]  # true

如果第二个陈述是True,则表示两个列表中的action都相同。

如果您已为每个subparser单独定义-n,则不会看到此内容。他们会有不同的行动对象。

我正在使用我对代码的了解,而不是文档中的任何内容。最近在Cause Python's argparse to execute action for default中指出,文档没有说明add_argument返回Action个对象。这些对象是代码组织的重要组成部分,但它们在文档中并没有引起太多关注。


如果使用“resolve”冲突处理程序,则通过引用复制父操作也会产生问题,并且需要重用父级。

中提出了这个问题

argparse conflict resolver for options in subcommands turns keyword argument into positional argument

和Python bug问题:

http://bugs.python.org/issue22401

对于这个问题而言,一个可能的解决方案是(可选)制作动作的副本,而不是共享引用。这样,可以在子节点中修改option_stringsdefaults,而不会影响父节点。

答案 1 :(得分:5)

发生了什么

这里的问题是解析器参数是对象,当解析器从它的父节点继承时,它会将父操作的引用添加到它自己的列表中。当您调用set_default时,它会设置此对象的默认值,该对象在子分析程序之间共享。

您可以检查子分析符以查看:

>>> a1 = [ action for action in subparser1._actions if action.dest=='n' ].pop()
>>> a2 = [ action for action in subparser2._actions if action.dest=='n' ].pop()
>>> a1 is a2 # same object in memory
True
>>> a1.default
20
>>> type(a1)
<class 'argparse._StoreAction'>

第一个解决方案:将此参数明确添加到每个子分析器

您可以通过将参数分别添加到每个子分析器而不是将其添加到基类来解决此问题。

subparser1= subparsers.add_parser('a', help='subparser 1', 
                               parents=[base_parser])
subparser1.add_argument('-n', help='number', type=int, default=50)
subparser2= subparsers.add_parser('b', help='subparser 2', 
                               parents=[base_parser])
subparser2.add_argument('-n', help='number', type=int, default=20)
...

第二个解决方案:多个基类

如果有许多子分析符共享相同的默认值,并且您希望避免这种情况,则可以为每个默认值创建不同的基类。由于parents是基类列表,您仍然可以将公共部分分组到另一个基类中,并将子分类器传递给多个基类继承。这可能是不必要的复杂。

import argparse

# this is the top level parser
parser = argparse.ArgumentParser(description='bla bla')

# this serves as a parent parser
base_parser = argparse.ArgumentParser(add_help=False)
# add common args

# for group with 50 default
base_parser_50 = argparse.ArgumentParser(add_help=False)
base_parser_50.add_argument('-n', help='number', type=int, default=50)

# for group with 50 default
base_parser_20 = argparse.ArgumentParser(add_help=False)
base_parser_20.add_argument('-n', help='number', type=int, default=20)

# subparsers
subparsers = parser.add_subparsers()
subparser1= subparsers.add_parser('a', help='subparser 1', 
                                   parents=[base_parser, base_parser_50])

subparser2 = subparsers.add_parser('b', help='subparser 2',
                                   parents=[base_parser, base_parser_20])

args = parser.parse_args()
print args

使用共享参数的第一个解决方案

您还可以共享参数的字典并使用解包来避免重复所有参数:

import argparse

# this is the top level parser
parser = argparse.ArgumentParser(description='bla bla')

n_args = '-n',
n_kwargs = {'help': 'number', 'type': int}

# subparsers
subparsers = parser.add_subparsers()
subparser1= subparsers.add_parser('a', help='subparser 1')
subparser1.add_argument(*n_args, default=50, **n_kwargs)

subparser2 = subparsers.add_parser('b', help='subparser 2')
subparser2.add_argument(*n_args, default=20, **n_kwargs)

args = parser.parse_args()
print args

答案 2 :(得分:0)

我也希望多个子解析器也可以继承公共参数,但是argparse的parents功能也给我带来了问题,正如其他人所解释的那样。幸运的是,有一个非常简单的解决方案:创建一个添加参数的函数,而不是创建父函数。

我将subparser1subparser2都传递给函数parent_parser,该函数添加了公用参数-n

import argparse

# this is the top level parser
parser = argparse.ArgumentParser(description='bla bla')

# this serves as a parent parser
def parent_parser(parser_to_update):
    parser_to_update.add_argument('-n', help='number', type=int)
    return parser_to_update


# subparsers
subparsers = parser.add_subparsers()
subparser1 = subparsers.add_parser('a', help='subparser 1')
subparser1 = parent_parser(subparser1)
subparser1.set_defaults(n=50)
subparser2 = subparsers.add_parser('b', help='subparser 2')
subparser2 = parent_parser(subparser2)
subparser2.set_defaults(n=20)

args = parser.parse_args()
print(args)

当我运行脚本时:

$ python subparse.py b
Namespace(n=20)

$ python subparse.py a
Namespace(n=50)