使用 argparse 解析对象列表

Use argparse to parse a list of objects

我有一个程序,其函数接受 class 初始值设定项和对象列表。每个对象由 3 个变量 id、value 和 tag 组成。

class Package():

    def __init__(self, id, value, name):
        if (value <= 0):
            raise ValueError("Amount must be greater than 0")
        self.id = id
        self.value = value
        self.tag = tag


 class Purchase():

    def submit(some_list):
        //Do stuff

def main():
    //Help here!
    parser = argparse.ArgumentParser()
    parser.add_argument("id", help="ID")
    parser.add_argument("value", help="Value")
    parser.add_argument("tag", help="Tag")
    args = parser.parse_args()
    some_list = [args.id, args.value, args.tag]
    submit(some_list)

我正在尝试在 main() 中实现 argparse,这样我就可以 运行 通过执行类似以下操作的程序:python foo.py "int0 [(int1, float1, int2), (int3, float2, int4) ....]"。列表中的对象数量是可变的,取决于用户输入。

initializer = num0

//First package object
package.id = num1
package.value = num2
package.tag = num3

//Second package object
package.id = num4
package.value = num5
package.tag = num6  

您可以自定义argument type and use ast.literal_eval()来解析值。

工作样本:

import argparse
from ast import literal_eval


class Package():
    def __init__(self, id, value, tag):
        if (value <= 0):
            raise ValueError("Amount must be greater than 0")
        self.id = id
        self.value = value
        self.tag = tag


def packages(s):
    try:
        data = literal_eval(s)
    except:  # TODO: avoid bare except and handle more specific errors
        raise argparse.ArgumentTypeError("Invalid 'packages' format.")

    return [Package(*item) for item in data]


parser = argparse.ArgumentParser()
parser.add_argument('--packages', dest="packages", type=packages, nargs=1)
args = parser.parse_args()
print(args.packages)

现在,如果您要 运行 脚本,您将得到 Package class 个打印实例的列表:

$ python test.py --packages="[(1, 1.02, 3), (40, 2.32, 11)]"
[[<__main__.Package instance at 0x10a20d368>, <__main__.Package instance at 0x10a20d4d0>]]

我希望更明确一点并使用自定义操作:

import argparse

class PackageAction(argparse.Action):
    def __init__(self, *args, **kwargs):
        super(PackageAction, self).__init__(*args, **kwargs)
        self.nargs = 3

    def __call__(self, parser, namespace, values, option_string):
        lst = getattr(namespace, self.dest, []) or []
        a, b, c = values
        lst.append(Package(int(a), float(b), int(c)))
        setattr(namespace, self.dest, lst)

class Package(object):
    def __init__(self, foo, bar, baz):
        self.foo = foo
        self.bar = bar
        self.baz = baz

    def __repr__(self):
        return 'Package(%r, %r, %r)' % (self.foo, self.bar, self.baz)

parser = argparse.ArgumentParser()
parser.add_argument('--package', action=PackageAction)

print(parser.parse_args())

此处的用法类似于:

$ python packager.py --package 1 2 3 --package 4 5 6
Namespace(package=[Package(1, 2.0, 3), Package(4, 5.0, 6)])

一个好处是您获得了更好的默认错误处理...例如:

$ python ~/sandbox/test.py --package 1 2 3 --package 4 5
usage: test.py [-h] [--package PACKAGE PACKAGE PACKAGE]
test.py: error: argument --package: expected 3 argument(s)

当然,您可以根据自己的目的进行修改——具体来说,向 __call__ 提供一些额外的 error handling 可能会更好。例如你可以做类似

的事情
parser.error('--package requires an int float and int')

如果用户传递了错误的字符串。您还可以提供更好的变量名称:-)

这是我的提名;它使用一个普通的解析器,并将自定义放在 Package class.

它将被调用为:

python prog.py -p 0 1 2 --package 2 3 4

其中 -p--package 后跟 3 个值,并且可以重复(action 是 'append')。 nargs=3 确保每个 -p 后跟 3 个值(否则解析器会引发错误)。将这些值转换为数字(并引发错误)是 Package class 的责任。 class 已经检查了 non-negative value.

import argparse
class Package():
    def __init__(self, id, value, tag):
        # 3 inputs - numbers, but equivalent strings are accepted
        # may add more value validation
        self.id = int(id)
        self.value = float(value)
        if self.value <= 0:
            raise ValueError("Amount must be greater than 0")
        self.tag = int(tag)
    def __repr__(self):
        return 'Package (%s, %s, %s)'%(self.id, self.value, self.tag)

def main(argv):
    parser = argparse.ArgumentParser()
    parser.add_argument('-p', '--package', nargs=3, action='append', default=[],
        metavar=('ID','Value','tag'), help='package parameters; may repeat')
    args = parser.parse_args(argv)
    print args
    packages = [Package(*v) for v in args.package]
    return packages
    # alt
    # args.package = packages; return args

if __name__ == '__main__':
    import sys
    if sys.argv[1:]:
        print main(sys.argv[1:])
    else:
        # test cases
        print main([]) # nothing
        print main('-p 1 2 3'.split())
        print main('-p 0 1 2 --pack 2 3 4'.split())
        print main(['-h']) # help

测试用例的样本 运行 是:

2030:~/mypy$ python stack34823075.py 
Namespace(package=[])
[]
Namespace(package=[['1', '2', '3']])
[Package (1, 2.0, 3)]
Namespace(package=[['0', '1', '2'], ['2', '3', '4']])
[Package (0, 1.0, 2), Package (2, 3.0, 4)]

usage: stack34823075.py [-h] [-p ID Value tag]

optional arguments:
  -h, --help            show this help message and exit
  -p ID Value tag, --package ID Value tag
                        package parameters; may repeat

请注意 metavar 如何影响帮助显示。 Package __repr__ 方法生成一个漂亮的列表显示。


运行 与 non-numeric tag 的示例:

2038:~/mypy$ python stack34823075.py -p 1 2.3 tag
Namespace(package=[['1', '2.3', 'tag']])
Traceback (most recent call last):
  File "stack34823075.py", line 31, in <module>
    print main(sys.argv[1:])
  File "stack34823075.py", line 20, in main
    packages = [Package(*v) for v in args.package if v is not None]
  File "stack34823075.py", line 10, in __init__
    self.tag = int(tag)
ValueError: invalid literal for int() with base 10: 'tag'

一个特殊的type函数在这里不能正常工作。它将单独应用于 3 个字符串中的每一个,而不是作为一个组。

自定义 Action class 可以处理这 3 个值,将每个值转换为 intfloatint。但即使在那里,我也更愿意将它们传递给 Package,例如

def __call__(self, namespace, dest, values):
    # store_action style
    new_value = Package(*values)
    setattr(namespace, dest, new_value) # store action

但是由于 packages = [Package(*v) for v in args.package] 是如此简单,我认为自定义解析器或其操作没有多大意义。