两个 Python argparse 对象可以合并吗?

Can two Python argparse objects be combined?

我有一个包含 parserA 的对象 A - 一个 argparse.ArgumentParser 对象 还有一个包含 parserB 的对象 B - 另一个 argparse.ArgumentParser

对象 A 包含对象 B 的一个实例,但是对象 B 的参数现在需要由对象 A 中的解析器解析(因为 A 是从命令行使用参数调用的对象,而不是 B)

有没有办法在Python对象A中写成:parserA += B.parserB?

您不能在另一个中使用一个 ArgumentParser。但是有办法解决。您需要提取到向解析器添加参数的方法代码。 然后您将能够使用它们来合并解析器中的参数。 此外,对参数(与其解析器相关)进行分组也会更容易。但是你必须支持参数名称集不相交。

示例:

foo.py:

def add_foo_params( group ):
   group.add_argument('--foo', help='foo help')

if __name__ = "__main__":  
   parser = argparse.ArgumentParser(prog='Foo')

boo.py

def add_boo_params( group ):
   group.add_argument('--boo', help='boo help')

if __name__ = "__main__":  
   parser = argparse.ArgumentParser(prog='Boo')

fooboo.py

   from foo import add_foo_params
   from boo import add_boo_params

   if __name__ = "__main__":  
       parser = argparse.ArgumentParser(prog='FooBoo')
       foo_group = parser.add_argument_group(title="foo params")
       boo_group = parser.add_argument_group(title="boo params")

       add_foo_params( foo_group )
       add_boo_params( boo_group )

argparse 是在 object 年代左右开发的。除了一些常量和实用函数外,它都是 class 定义。该文档侧重于使用而不是 class 结构。但这可能有助于理解其中的一点。

parser = argparse.ArgumentParser(...)

创建 parser object.

arg1 = parser.add_argument(...)

创建一个 argparse.Action(实际上是子 class)object 并将其添加到几个 parser 属性(列表)中。通常我们会忽略方法 return 这个 Action object 的事实,但偶尔我会发现它很有帮助。当我在交互式 shell 中构建解析器时,我看到了这个动作。

args = parser.parse_args()

运行另一个方法,并且 return 是一个命名空间 object (class argparse.Namespace)。

组方法和子解析器方法还创建 return objects(组、操作 and/or 解析器)。

ArgumentParser 方法接受一个 parents 参数,其中值是解析器 object 的列表。

parsera = argparse.ArgumentParser(parents=[parserb])

parsera 的创建过程中,parserb 中的操作和组被复制到 parsera。这样,parsera 将识别 parserb 所做的所有参数。我鼓励你测试一下。

但是有一些条件。副本是引用。也就是说,parsera 得到一个指向 parserb 中定义的每个 Action 的指针。偶尔会产生问题(我现在不谈)。一个或另一个必须有 add_help=False。通常在创建时将帮助操作添加到解析器。但是,如果 parserb 也有帮助,则会出现必须解决的冲突(重复)。

但是如果 parsera 是独立于 parserb 创建的,则无法使用 parents。没有用于从 parserb 添加操作的现有机制。有可能制作一个新的解析器,同时使用 parents

parserc = argparse.ArgumentParser(parents=[parsera, parserb])

我可能会编写一个函数,将参数从 parserb 添加到 parsera,借鉴实现 parents 的方法的想法。但我必须知道如何解决冲突。

查看 argparse._ActionsContainer._add_container_actions 以了解参数(操作)如何从 parent 复制到 parser。可能令人困惑的是,每个 Action 除了在 parser.[=46 中之外,还是 group(用户定义的或 2 个默认组之一(见帮助))的一部分=]

另一种可能是使用

[argsA, extrasA] = parserA.parse_known_args()
[argsB, extrasB] = parserB.parse_known_args()  # uses the same sys.argv 
# or
args = parserB.parse_args(extrasA, namespace=argsA)

有了这个,每个解析器都会处理它知道的参数,return 其余的在 extras 列表中。

除非解析器是为这种集成设计的,否则这种集成会有一些粗糙的边缘。使用 Arnial's 方法可能更容易处理这些冲突,即将共享参数定义放在您自己的方法中。其他人喜欢将参数参数放在某种数据库(列表、字典等)中,并从中构建解析器。您可以根据需要将解析器创建包装在尽可能多的样板层中。

对于您的用例,如果可以的话,您可以尝试通过专用方法在 classes 之间简单地共享相同的 argparse 对象。 以下是根据您的情况得出的。

import argparse

class B(object):

  def __init__(self, parserB=argparse.ArgumentParser()):
    super(B, self).__init__()
    self.parserB = parserB

  def addArguments(self):
    self.parserB.add_argument("-tb", "--test-b", help="Test B", type=str, metavar="")
    #Add more arguments specific to B

  def parseArgs(self):
    return self.parserB.parse_args()

class A(object):

  def __init__(self, parserA=argparse.ArgumentParser(), b=B()):
    super(A, self).__init__()
    self.parserA = parserA
    self.b = b

  def addArguments(self):
    self.parserA.add_argument("-ta", "--test-a", help="Test A", type=str, metavar="")
    #Add more arguments specific to A

  def parseArgs(self):
    return self.parserA.parse_args()

  def mergeArgs(self):
    self.b.parserB = self.parserA
    self.b.addArguments()
    self.addArguments()

代码解释:

  • 如前所述,在问题中,对象 A 和对象 B 包含它们自己的解析器对象。对象 A 还包含对象 B 的一个实例。
  • 该代码只是将预期的流程分成单独的方法,这样就可以在尝试解析之前继续向单个解析器添加参数。

测试个人

a = A()
a.addArguments()
print(vars(a.parseArgs()))

# CLI Command
python test.py -ta "Testing A"

# CLI Result
{'test_a': 'Testing A'}

综合测试

aCombined = A()
aCombined.mergeArgs()
print(vars(aCombined.parseArgs()))

# CLI Command
testing -ta "Testing A" -tb "Testing B"

# CLI Result
{'test_b': 'Testing B', 'test_a': 'Testing A'}

额外

您还可以制作一个通用方法,该方法采用可变参数,并迭代并不断添加各种 classes 的参数。我使用通用的“解析器”属性名称为下面的示例创建了 class C 和 D。

多项测试

# Add method to Class A
def mergeMultiArgs(self, *objects):
    parser = self.parserA
    for object in objects:
        object.parser = parser
        object.addArguments()
    self.addArguments()

aCombined = A()
aCombined.mergeMultiArgs(C(), D())
print(vars(aCombined.parseArgs()))

# CLI Command
testing -ta "Testing A" -tc "Testing C" -td "Testing D"

# CLI Result
{'test_d': 'Testing D', 'test_c': 'Testing C', 'test_a': 'Testing A'}

是的,它们可以合并,这样做:

这是一个合并两个参数的函数:

def merge_args_safe(args1: Namespace, args2: Namespace) -> Namespace:
    """
    Merges two namespaces but throws an error if there are keys that collide.

    ref: 
    :param args1:
    :param args2:
    :return:
    """
    # - the merged args
    # The vars() function returns the __dict__ attribute to values of the given object e.g {field:value}.
    args = Namespace(**vars(args1), **vars(args2))
    return args

测试

def merge_args_test():
    args1 = Namespace(foo="foo", collided_key='from_args1')
    args2 = Namespace(bar="bar", collided_key='from_args2')

    args = merge_args(args1, args2)
    print('-- merged args')
    print(f'{args=}')

输出:

Traceback (most recent call last):
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/pydevd.py", line 1483, in _exec
    pydev_imports.execfile(file, globals, locals)  # execute the script
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/_pydev_imps/_pydev_execfile.py", line 18, in execfile
    exec(compile(contents+"\n", file, 'exec'), glob, loc)
  File "/Users/brando/ultimate-utils/ultimate-utils-proj-src/uutils/__init__.py", line 1202, in <module>
    merge_args_test()
  File "/Users/brando/ultimate-utils/ultimate-utils-proj-src/uutils/__init__.py", line 1192, in merge_args_test
    args = merge_args(args1, args2)
  File "/Users/brando/ultimate-utils/ultimate-utils-proj-src/uutils/__init__.py", line 1116, in merge_args
    args = Namespace(**vars(args1), **vars(args2))
TypeError: argparse.Namespace() got multiple values for keyword argument 'collided_key'
python-BaseException

你可以在这个图书馆找到它:https://github.com/brando90/ultimate-utils


如果您想解决冲突,请执行以下操作:

def merge_two_dicts(starting_dict: dict, updater_dict: dict) -> dict:
    """
    Starts from base starting dict and then adds the remaining key values from updater replacing the values from
    the first starting/base dict with the second updater dict.

    For later: how does d = {**d1, **d2} replace collision?

    :param starting_dict:
    :param updater_dict:
    :return:
    """
    new_dict: dict = starting_dict.copy()   # start with keys and values of starting_dict
    new_dict.update(updater_dict)    # modifies starting_dict with keys and values of updater_dict
    return new_dict

def merge_args(args1: Namespace, args2: Namespace) -> Namespace:
    """

    ref: 
    :param args1:
    :param args2:
    :return:
    """
    # - the merged args
    # The vars() function returns the __dict__ attribute to values of the given object e.g {field:value}.
    merged_key_values_for_namespace: dict = merge_two_dicts(vars(args1), vars(args2))
    args = Namespace(**merged_key_values_for_namespace)
    return args

测试:

def merge_args_test():
    args1 = Namespace(foo="foo", collided_key='from_args1')
    args2 = Namespace(bar="bar", collided_key='from_args2')

    args = merge_args(args1, args2)
    print('-- merged args')
    print(f'{args=}')
    assert args.collided_key == 'from_args2', 'Error in merge dict, expected the second argument to be the one used' \
                                                 'to resolve collision'