Python argparse 中任意数量参数的自定义解析函数

Custom parsing function for any number of arguments in Python argparse

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

./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

应该有这样的字典:

{'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'})

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

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

Create a new object each time the function is called, by using a default arg to signal that no argument was provided (None is often a good choice).

很明显 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='*',因为我发现如果你这样称呼它,它在这里比有用更危险,并且还删除了 values 上的错误循环,总是使用 values :

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

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

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

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

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

call_AppendAction class 可能有指导意义:

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),则它 returns 是一个空列表。所以它确保追加以列表开头。

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

指出的问题

因此 'append action' 在 call 本身中定义了初始空列表。并使用 copy 来避免修改任何其他默认值。

你想要 values 而不是 value 吗?

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

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

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

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

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