我有一个很大的程序,它有一个基于argparse
的CLI交互,有几个子解析器。 subparsers参数支持的选项列表是根据数据库查询,解析不同的xml文件,进行不同的计算等确定的,因此它非常耗费IO并且非常耗时。
问题在于,当我运行脚本时,argparse似乎为所有子解析器提取choices
,这会增加相当大且烦人的启动延迟。
有没有办法让argparse
仅为当前使用的子解析器获取并验证choices
?
一种解决方案可能是将所有验证逻辑更深入地移动到代码中,但如果可能的话,这将意味着我要避免的大量工作。
谢谢
答案 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__
)很简单。可以使用help
和metavar
属性自定义帮助/使用情况。自定义错误消息更难。 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
,则它将初始化。