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

16 投票
3 回答
12683 浏览
提问于 2025-04-18 12:44

我想在一个脚本里定义不同的子解析器,它们都从一个共同的父解析器那里继承选项,但每个子解析器有不同的默认值。不过,这并没有按我预期的那样工作。

这是我做的:

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 个回答

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)
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
11

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 对象。这些对象在代码组织中很重要,但在文档中并没有得到足够的关注。


通过引用复制父级操作也会在使用“解决”冲突处理程序时产生问题,如果父级需要被重用。这个问题在

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

和 Python 的 bug 问题中提到:

http://bugs.python.org/issue22401

一个可能的解决方案,针对这个问题和那个问题,是(可选地)复制操作,而不是共享引用。这样,option_stringsdefaults 可以在子类中修改,而不会影响父类。

撰写回答