argparse按需进口类型,选择等

时间:2014-03-30 09:39:09

标签: python argparse

我有一个很大的程序,它有一个基于argparse的CLI交互,有几个子解析器。 subparsers参数支持的选项列表是根据数据库查询,解析不同的xml文件,进行不同的计算等确定的,因此它非常耗费IO并且非常耗时。

问题在于,当我运行脚本时,argparse似乎为所有子解析器提取choices,这会增加相当大且烦人的启动延迟。

有没有办法让argparse仅为当前使用的子解析器获取并验证choices

一种解决方案可能是将所有验证逻辑更深入地移动到代码中,但如果可能的话,这将意味着我要避免的大量工作。

谢谢

4 个答案:

答案 0 :(得分:2)

要延迟提取选择,可以分两个阶段解析命令行:在第一阶段,只找到subparser,在第二阶段,subparser用于解析其余参数:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('subparser', choices=['foo','bar'])

def foo_parser():
    parser = argparse.ArgumentParser()
    parser.add_argument('fooval', choices='123')
    return parser

def bar_parser():
    parser = argparse.ArgumentParser()
    parser.add_argument('barval', choices='ABC')
    return parser

dispatch = {'foo':foo_parser, 'bar':bar_parser}
args, unknown = parser.parse_known_args()
args = dispatch[args.subparser]().parse_args(unknown)
print(args)

可以像这样使用:

% script.py foo 2
Namespace(fooval='2')

% script.py bar A
Namespace(barval='A')

请注意,顶级帮助消息不太友好,因为它只能告诉您子分析程序的选择:

% script.py -h
usage: script.py [-h] {foo,bar}
...

要查找每个子分析器中的选项信息,用户必须选择子分析器并将-h传递给它:

% script.py bar -- -h
usage: script.py [-h] {A,B,C}

--之后的所有参数都被视为非选项(script.py),因此由bar_parser解析。

答案 1 :(得分:1)

这是一个“懒惰”选择的快速而肮脏的例子。在这种情况下,选项是一系列整数。我认为需要昂贵的数据库查找的案例可以以类似的方式实现。

# argparse with lazy choices

class LazyChoice(object):
    # large range
    def __init__(self, argmax):
        self.argmax=argmax
    def __contains__(self, item):
        # a 'lazy' test that does not enumerate all choices
        return item<=self.argmax
    def __iter__(self):
        # iterable for display in error message
        # use is in:
        # tup = value, ', '.join(map(repr, action.choices))
        # metavar bypasses this when formatting help/usage
        return iter(['integers less than %s'%self.argmax])

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--regular','-r',choices=['one','two'])
larg = parser.add_argument('--lazy','-l', choices=LazyChoice(10))
larg.type = int
print parser.parse_args()

实施测试部分(__contains__)很简单。可以使用helpmetavar属性自定义帮助/使用情况。自定义错误消息更难。 http://bugs.python.org/issue16468讨论choices无法迭代时的替代方案。 (也可选择长列表:http://bugs.python.org/issue16418

我还展示了在初始设置后如何更改type。这并没有解决基于subparser选择设置类型的问题。但是编写自定义type并不难,可以进行某种Db查找。所有type函数需要做的是获取一个字符串,返回正确的转换值,如果出现问题则引发ValueError

答案 2 :(得分:0)

这是一个测试延迟创建subparser直到实际需要的想法的脚本。理论上,它可以通过仅创建实际需要的子分析器来节省启动时间。

我使用nargs=argparse.PARSER复制主解析器中的subparser行为。 help行为类似。

# lazy subparsers test
# lazy behaves much like a regular subparser case, but only creates one subparser
# for N=5 time differences do not rise above the noise

import argparse

def regular(N):
    parser = argparse.ArgumentParser()
    sp = parser.add_subparsers(dest='cmd')
    for i in range(N):
        spp = sp.add_parser('cmd%s'%i)
        spp.set_defaults(func='cmd%s'%(10*i))
        spp.add_argument('-f','--foo')
        spp.add_argument('pos', nargs='*')
    return parser

def lazy(N):
    parser = argparse.ArgumentParser()
    sp = parser.add_argument('cmd', nargs=argparse.PARSER, choices=[])
    for i in range(N):
        sp.choices.append('cmd%s'%i)
    return parser

def subpar(cmd):
    cmd, argv = cmd[0], cmd[1:]
    parser = argparse.ArgumentParser(prog=cmd)
    parser.add_argument('-f','--foo')
    parser.add_argument('pos', nargs='*')
    parser.set_defaults(func=cmd)
    args = parser.parse_args(argv)
    return args

N = 5
mode = True #False
argv = 'cmd1 -f1 a b c'.split()
if mode:
    args = regular(N).parse_args(argv)
    print(args)
else:
    args = lazy(N).parse_args(argv)
    print(args)
    if isinstance(args.cmd, list):
        sargs = subpar(args.cmd)
        print(sargs)

使用mode(和N = 5)

的不同值运行测试
1004:~/mypy$ time python3 stack44315696.py 
Namespace(cmd='cmd1', foo='1', func='cmd10', pos=['a', 'b', 'c'])

real    0m0.052s
user    0m0.044s
sys 0m0.008s
1011:~/mypy$ time python3 stack44315696.py 
Namespace(cmd=['cmd1', '-f1', 'a', 'b', 'c'])
Namespace(foo='1', func='cmd1', pos=['a', 'b', 'c'])

real    0m0.051s
user    0m0.048s
sys 0m0.000s

N必须要大得多才能看到效果。

答案 3 :(得分:0)

我已经通过创建一个简单的ArgumentParser子类解决了该问题:

import argparse

class ArgumentParser(argparse.ArgumentParser):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.lazy_init = None

    def parse_known_args(self, args=None, namespace=None):
        if self.lazy_init is not None:
            self.lazy_init()
            self.lazy_init = None

        return super().parse_known_args(args, namespace)

然后我可以按以下方式使用它:

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='command', title='commands', parser_class=ArgumentParser)
subparsers.required = True

subparser = subparsers.add_parser(
    'do-something', help="do something",
    description="Do something great.",
)

def lazy_init():
    from my_database import data

    subparser.add_argument(
        '-o', '--option', choices=data.expensive_fetch(), action='save',
    )

subparser.lazy_init = lazy_init

仅当父解析器尝试解析子解析器的参数时,这才真正初始化子解析器。因此,如果您执行program -h,则不会初始化子解析器,但是如果您执行program do-something -h,则它将初始化。