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
把第一个的设置给覆盖了。因为在argparse的文档里没有提到这一点(文档其实很详细),我以为这可能是个bug。
有没有什么简单的解决办法呢?我可以在之后检查 args
变量,把 None
的值替换成每个子解析器的预期默认值,但我本来是希望argparse能帮我做到这一点。
顺便说一下,这是Python 2.7的内容。
3 个回答
我想让多个子解析器共享一些共同的参数,但使用argparse里的parents
功能时遇到了一些问题,其他人也有类似的经历。幸运的是,有一个非常简单的解决办法:创建一个函数来添加参数,而不是创建一个父解析器。
我把subparser1
和subparser2
都传给一个叫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)
发生了什么
这里的问题是,解析器的参数是对象。当一个解析器从它的父类继承时,它会把父类的动作引用添加到自己的列表中。当你调用 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
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
列表中。它并不是定义新的操作,而只是复制了指向同一个操作的指针。
所以当你在 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
对象。这些对象在代码组织中很重要,但在文档中并没有得到足够的关注。
通过引用复制父级操作也会在使用“解决”冲突处理程序时产生问题,如果父级需要被重用。这个问题在
和 Python 的 bug 问题中提到:
http://bugs.python.org/issue22401
一个可能的解决方案,针对这个问题和那个问题,是(可选地)复制操作,而不是共享引用。这样,option_strings
和 defaults
可以在子类中修改,而不会影响父类。