我有一个Python包,里面有很多子命令和子子命令。它的组织方式有点像这样:
main.py
import argparse
from sum import prepare_arg_parser as prepare_sum_parser
from sub import prepare_arg_parser as prepare_sub_parser
parser = argparse.ArgumentParser()
sub_parsers = parser.add_subparsers(dest="command", required=True)
prepare_sum_parser(sub_parsers.add_parser("sum"))
prepare_sub_parser(sub_parsers.add_parser("sub"))
args = parser.parse_args()
args.func(args)
sum.py
import argparse
def prepare_arg_parser(parser):
parser.set_defaults(func=do_sum)
parser.add_argument("a", type=int)
parser.add_argument("b", type=int)
def do_sum(args):
print(f"{args.a} + {args.b} = {args.a + args.b}")
等等。
但是,随着模块列表的增长,启动时间也会增加。 我想延迟加载每个子命令,因此它仅加载所选命令所需的内容。
我尝试过这样的事情:
import argparse
def do_sum(args):
from sum import prepare_arg_parser
parser = argparse.ArgumentParser()
prepare_arg_parser(parser)
sum_args = parser.parse_args(args)
sum_args.func(sum_args)
parser = argparse.ArgumentParser()
sub_parsers = parser.add_subparsers(dest="command", required=True)
sub_parsers.add_parser("sum").set_defaults(func=do_sum)
args, rest = parser.parse_known_args()
args.func(rest)
但是帮助和错误文本不正确。
另一种方法是将每个子命令拆分为一个“prepare argparse”模块,该模块会知道所有参数,但会延迟调用 do_sum 函数,但我不喜欢它,因为它将参数声明与其使用分开。而且它仍然会加载很多不必要的文件。
是否有延迟加载的子命令?
与导入许多模块的成本相比,创建解析器和子解析器的成本很便宜。我解决这个问题的方法是在
main.py
中创建解析器和子解析器,并将实际操作留在模块中。
让我们从主模块开始:
# main.py
import argparse
import importlib
def parse_command_line():
parser = argparse.ArgumentParser()
sub_parsers = parser.add_subparsers(dest="command", required=True)
sum_parser = sub_parsers.add_parser("sum")
sum_parser.add_argument("a", type=int)
sum_parser.add_argument("b", type=int)
return parser.parse_args()
def main():
args = parse_command_line()
command = importlib.import_module(args.command)
command.main(args)
if __name__ == "__main__":
main()
在本模块中,我们创建了解析器和一个子解析器(sum)。我们可以稍后添加更多子解析器。
解析命令行后,
args.command
包含解析器的名称,例如“和”。计划是有一个具有该名称的模块,即 sum.py
。在该模块中,我们应该有一个函数 main()
来执行以下操作:
# sum.py
import argparse
def main(args: argparse.Namespace):
print(f"{args.a} + {args.b} = {args.a + args.b}")
我喜欢这种方法的原因:
parse_command_line()
功能会增长,main()
功能保持不变。main()
功能而不用担心解析命令行。我不喜欢的:
parse_command_line()
函数在这种方法中,让我们建立一个简单的“插件”系统。除了主模块之外,我们还有插件,每个插件都会满足这些要求:
sub_<name>.py
。例如:sub_sum.py
、sub_dbl.py
、...让我们从主模块开始:
# main.py
import argparse
import pathlib
import subprocess
import sys
def parse_command_line():
# Determine the choices
main_dir = pathlib.Path(__file__).parent
choices = [
path.stem.removeprefix("plugin_") for path in main_dir.glob("plugin_*.py")
]
parser = argparse.ArgumentParser()
parser.add_argument("command", choices=choices)
return parser.parse_known_args()
def main():
args, remainder = parse_command_line()
command = [sys.executable, f"plugin_{args.command}.py"] + remainder
subprocess.run(command)
if __name__ == "__main__":
main()
以下是“sum”插件的内容:
# plugin_sum.py
import argparse
def main():
parser = argparse.ArgumentParser()
parser.add_argument("a", type=int)
parser.add_argument("b", type=int)
args = parser.parse_args()
print(f"{args.a} + {args.b} = {args.a + args.b}")
if __name__ == "__main__":
main()
那么它是如何运作的呢?主模块仅收集插件的名称并使用
subprocess.run()
调用正确的插件。每个插件都是一个独立的脚本,能够独立运行。
我喜欢什么:
我不喜欢的:
--help
标志没有提供太多帮助,因为它不了解插件我更喜欢简单的方法,因为它简单。插件方法对于更复杂的结构更有效。这两种方法都不会加载所有模块才能工作。