【问题标题】:Custom parsing function for any number of arguments in Python argparsePython argparse 中任意数量参数的自定义解析函数
【发布时间】:2015-10-24 01:02:54
【问题描述】:

我有一个通过命令行获取命名参数的脚本。可以多次提供参数之一。比如我想运行一个脚本:

./script.py --add-net=user1:10.0.0.0/24 --add-net=user2:10.0.1.0/24 --add-net=user3:10.0.2.0/24

现在我想要一个 argparse 动作来解析每个参数并将结果存储在一个字典中,例如:

{ 'user1': '10.0.0.0/24',
  'user2': '10.0.1.0/24',
  'user3': '10.0.2.0/24' }

如果没有提供值,还应该提供一个默认值。喜欢

./script.py

应该有这样的dict:

{'user': '192.168.0.0/24'}

我相信我必须为 argparse 构建一个自定义操作。我想出的是:

class ParseIPNets(argparse.Action):
    """docstring for ParseIPNets"""
    def __init__(self, option_strings, dest, nargs=None, **kwargs):
        super(ParseIPNets, self).__init__(option_strings, dest, **kwargs)

    def __call__(self, parser, namespace, values, option_string=None):
        for value in values:
            location, subnet = values.split(':')
            namespace.user_nets[location] = subnet

parser = argparse.ArgumentParser(description='foo')
parser.add_argument('--add-net',
                    nargs='*',
                    action=ParseIPNets,
                    dest='user_nets',
                    help='Nets subnets for users. Can be used multiple times',
                    default={"user1": "198.51.100.0/24"})

args = parser.parse_args()

当我需要使用默认值时效果很好:

test.py
Namespace(user_nets={'user1': '198.51.100.0/24'})

但是,当我添加参数时 - 它们被附加到默认值。我的期望是它们应该被添加到一个空的字典中:

test.py --add-net=a:10.0.0.0/24 --add-net=b:10.1.0.0/24
Namespace(user_nets={'a': '10.0.0.0/24', 'b': '10.1.0.0/24', 'user1': '198.51.100.0/24'})

达到我需要的正确方法是什么?

【问题讨论】:

  • 您为什么认为它们会被添加到空字典中?你提供了一个可变的论点;如果用户没有提供任何东西,也许将其作为后续步骤会更好?
  • 这正是我犯的错误。 @serge-ballesta 提供的解决方案就像一个魅力。

标签: python argparse


【解决方案1】:

很明显argparse在内部将默认值作为结果对象的初始值,你不应该在add_argument调用中直接设置默认值,而是做一些额外的处理:

parser.add_argument('--add-net',
                    action=ParseIPNets,
                    dest='user_nets',
                    help='Nets subnets for users. Can be used multiple times',
                    default = {})

args = parser.parse_args()
if len(args.user_nets) == 0:
    args.user_nets['user1'] = "198.51.100.0/24"

或者,如果您想要更好的用户体验,您可以利用 Python 处理可变默认参数的方式:

class ParseIPNets(argparse.Action):
    """docstring for ParseIPNets"""
    def __init__(self, option_strings, dest, nargs=None, **kwargs):
        super(ParseIPNets, self).__init__(option_strings, dest, **kwargs)
    def __call__(self, parser, namespace, values, option_string=None, first=[True]):
        if first[0]:
            namespace.user_nets.clear()
            first[0] = False
        location, subnet = values.split(':')
        namespace.user_nets[location] = subnet

parser.add_argument('--add-net',
                    action=ParseIPNets,
                    dest='user_nets',
                    help='Nets subnets for users. Can be used multiple times',
                    default={"user1": "198.51.100.0/24"})

args = parser.parse_args()

这样,如果选项存在,可选的默认值将被清除。

但是注意:这只会在第一次调用脚本时起作用。这里可以接受,因为parser.parse_args() 只能在脚本中调用一次。

附注:我删除了nargs='*',因为如果你这样称呼它,我发现它在这里比有用更危险,并且还删除了始终使用valuesvalues 上的错误循环:

test.py --add-net=a:10.0.0.0/24 --add-net=b:10.1.0.0/24

nargs='*' 对以下语法有意义:

test.py --add-net a:10.0.0.0/24 b:10.1.0.0/24

代码是:

    def __call__(self, parser, namespace, values, option_string=None, first=[True]):
        if first[0]:
            namespace.user_nets.clear()
            first[0] = False
        for value in values:
            location, subnet = value.split(':')
            namespace.user_nets[location] = subnet

【讨论】:

    【解决方案2】:

    使用可变的默认参数(在您的情况下为 dict)通常不是一个好主意,请参阅 here 以获得解释:

    每次调用函数时创建一个新对象,通过使用 默认 arg 表示没有提供任何参数(None 通常是 不错的选择)。

    【讨论】:

      【解决方案3】:

      我解决这个问题的第一种方法是使用action='append',并在解析后将结果列表转换为字典。代码量也差不多。

      'append' 对于默认值确实有同样的问题。如果default=['defaultstring'],则列表也将以该值开头。我会通过使用默认默认值([] 见下文)来解决这个问题,并在后期处理中添加默认值(如果列表仍然为空或无)。

      关于默认值的说明。在parse_args 的开头,所有操作默认值都添加到命名空间(除非将命名空间作为参数提供给parse_args)。然后解析命令行,每个操作对命名空间做自己的事情。最后,使用type 函数转换任何剩余的字符串默认值。

      在您的情况下,namespace.user_nets[location] = subnet 找到 user_nets 属性,并添加新条目。该属性默认初始化为字典,因此默认值出现在最终字典中。事实上,如果您将默认设置为 None 或某个字符串,您的代码将无法正常工作。

      _AppendAction 类的 call 可能具有指导意义:

      def __call__(self, parser, namespace, values, option_string=None):
          items = _copy.copy(_ensure_value(namespace, self.dest, []))
          items.append(values)
          setattr(namespace, self.dest, items)
      

      _ensure_valueargparse 中定义的函数。 _copy 是它导入的标准 copy 模块。

      _ensure_value 的作用类似于字典get(key, value, default),除了namespace 对象。在这种情况下,如果没有 self.dest 的值(或值为 None),它会返回一个空列表。所以它确保追加以列表开头。

      _copy.copy 确保将值附加到副本。这样,parse_args 将不会修改 default。它避免了@miles82提到的问题。

      所以“附加操作”定义了call 本身中的初始空列表。并使用copy 来避免修改任何其他默认值。

      您想要values 而不是value 吗?

      location, subnet = values.split(':')
      

      我倾向于将此转换放在类型函数中,例如

      def dict_type(astring):
         key, value = astring.split(':')
         return {key:value}
      

      这也是进行错误检查的好地方。

      在操作或解析后,这些可以添加到现有字典中update

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-12-14
        • 2014-03-04
        • 2017-10-31
        • 2015-03-04
        • 2021-11-14
        • 2014-05-20
        • 2015-07-20
        • 1970-01-01
        相关资源
        最近更新 更多