【问题标题】:Can two Python argparse objects be combined?可以组合两个 Python argparse 对象吗?
【发布时间】:2016-10-29 06:51:00
【问题描述】:

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

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

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

【问题讨论】:

  • 你能给出一个更具体的minimal reproducible example吗?为什么你有两个独立的解析器?
  • @Rogalski 。对我来说看起来不像重复。如果我理解正确,请重复更多关于选择不同方式解析参数的问题。这个关于如何合并它们的问题。
  • 技术上,所有信息都可以在parser._action_groups...
  • @jonrsharpe 一个用例是将 args 解析保存为机器学习实验中的 pickle 文件,然后为该实验加载先前的 args 解析,但是您的数据分析代码也有自己的参数来解析和那么你想将两者结合起来。

标签: python python-2.7 argparse


【解决方案1】:

您不能在另一个内部使用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 )

【讨论】:

    【解决方案2】:

    argparse 是围绕对象开发的。除了一些常量和实用函数之外,它都是类定义。该文档侧重于使用而不是该类结构。不过稍微了解一下可能会有所帮助。

    parser = argparse.ArgumentParser(...)
    

    创建一个parser 对象。

    arg1 = parser.add_argument(...)
    

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

    args = parser.parse_args()
    

    运行另一个方法,并返回一个命名空间对象(类argparse.Namespace)。

    组方法和子解析器方法也创建和返回对象(组、动作和/或解析器)。

    ArgumentParser 方法采用parents 参数,其中的值是解析器对象的列表。

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

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

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

    但如果parsera 是独立于parserb 创建的,则不能使用parents。没有从parserb 添加操作的现有机制。有可能制作一个新的解析器,两者都是父母

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

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

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

    另一种可能是使用

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

    每个解析器都处理它知道的参数,并在extras 列表中返回其余参数。

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

    【讨论】:

      【解决方案3】:

      对于您的用例,如果可以,您可以尝试通过专用方法在类之间简单地共享相同的 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'}
      

      附加

      您还可以创建一个采用可变参数的通用方法,并会迭代并不断添加各种类的参数。我使用通用的“解析器”属性名称为下面的示例创建了 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'}
      

      【讨论】:

        【解决方案4】:

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

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

        def merge_args_safe(args1: Namespace, args2: Namespace) -> Namespace:
            """
            Merges two namespaces but throws an error if there are keys that collide.
        
            ref: https://stackoverflow.com/questions/56136549/how-can-i-merge-two-argparse-namespaces-in-python-2-x
            :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: https://stackoverflow.com/questions/56136549/how-can-i-merge-two-argparse-namespaces-in-python-2-x
            :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'
        

        【讨论】:

          猜你喜欢
          • 2019-07-10
          • 2015-07-18
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-12-11
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多