Argparse - объединение родительского парсера, подпараметров и значений по умолчанию
Я хотел бы определить разные подпарамеры в script, причем оба варианта наследования от общего родителя, но с разными значениями по умолчанию. Однако он работает не так, как ожидалось.
Вот что я сделал:
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
Когда я запускаю script из командной строки, это то, что я получаю:
$ python subparse.py b
Namespace(n=20)
$ python subparse.py a
Namespace(n=20)
По-видимому, второй set_defaults
перезаписывает первый в родительском. Поскольку в документации argparse (что довольно подробно) не было ничего об этом, я подумал, что это может быть ошибкой.
Есть ли для этого какое-то простое решение? После этого я мог бы проверить переменную args
и заменить значения None
на значения по умолчанию для каждого подпарамера, но это то, что я ожидал от argparse для меня.
Это, кстати, Python 2.7.
Ответы
Ответ 1
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
(h
является первым).
subparser1._actions[1].dest # 'n'
subparser1._actions[1] is subparser2._actions[1] # true
Если этот 2-й оператор равен True
, это означает, что тот же action
находится в обоих списках.
Если вы определили -n
отдельно для каждого подпарамера, вы не увидите этого. У них будут разные объекты действий.
Я работаю от своих знаний о коде, а не в документации. В последнее время было указано, что вызывать аргумент Python для выполнения действия по умолчанию, в документации ничего не говорится о add_argument
, возвращающем объект action
. Эти объекты являются важной частью организации кода, но они не получают большого внимания в документации.
Копирование родительских действий по ссылке также создает проблемы, если используется обработчик конфликта "разрешить", и родительский элемент необходимо повторно использовать. Эта проблема была поднята в
argparse resolver для параметров в подкомандах превращает аргумент ключевого слова в позиционный аргумент
и проблема с ошибкой Python:
http://bugs.python.org/issue22401
Возможное решение, как для этой проблемы, так и для того, чтобы (необязательно) сделать копию действия, а не передавать ссылку. Таким образом, option_strings
и defaults
могут быть изменены у детей, не затрагивая родителя.
Ответ 2
Что происходит
Проблема здесь в том, что аргументы парсера являются объектами, и когда парсер наследует от него родителей, он добавляет ссылку на родительское действие в собственный список. Когда вы вызываете 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)
...
Второе решение: несколько базовых классов
Если есть много подпараметров, которые имеют одинаковое значение по умолчанию, и вы хотите избежать этого, вы можете создавать разные базовые классы для каждого значения по умолчанию. Поскольку родители представляют собой список базовых классов, вы все равно можете группировать общие части в другой базовый класс и передавать унаследованные из нескольких базовых классов subparser. Это, вероятно, излишне сложно.
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