argparse - 结合父解析器、子解析器和默认值

16

我希望在脚本中定义不同的子解析器,并且这些子解析器都继承自一个通用的父级选项,但是它们具有不同的默认值。然而,它并没有按照预期工作。

以下是我的操作:

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会覆盖父级中的第一个set_defaults。由于argparse文档中没有任何相关内容(其非常详细),我认为这可能是一个bug。

有没有简单的解决方案?我可以在检查完args变量后,将None值替换为每个子解析器的预期默认值,但我本来希望argparse能够自动完成这个任务。

顺便说一下,这是Python 2.7。

3个回答

11

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

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

你定义base_parser时创建了你的-n参数(一个action对象)。每次使用parents创建子解析器时,该操作将添加到每个子解析器的._actions列表中。它不定义新的操作,只是复制指针。

因此,当你在subparser2上使用set_defaults时,你修改了这个共享操作的default值。

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

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

如果第二个语句为True,那意味着相同的action在两个列表中都存在。

如果您为每个子解析器单独定义了-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
谢谢,我也怀疑是这样。我决定在解析参数后处理默认值。 - erickrf

8

发生了什么

问题在于解析器参数是对象,并且当解析器继承自其父项时,它会将对父项操作的引用添加到自己的列表中。当调用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)
...

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

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

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

感谢您提供详细的答案。由于我有超过两个子解析器和许多共享参数(包括不止一个父级),因此我决定创建一个函数,在解析参数后设置默认值。 - erickrf

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)

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