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