共计 6828 个字符,预计需要花费 18 分钟才能阅读完成。
当你想实现一个命令行程序时,或者第一个想到的是用 Python 来实现。比方 CentOS 上赫赫有名的包管理工具 yum
就是基于 Python 实现的。
而 Python 的世界中有很多命令行库,每个库都各具特色。但咱们往往不晓得其背地的设计理念,也因而在抉择时感到迷茫。这些库的作者为何在反复造轮子,他是从哪个角度来思考,来让命令行库“演变”到一个新的更好用的状态。
为了可能更加直观地感触到命令行库的设计理念,在此之前,咱们无妨设计一个名为 calc
的命令行程序,它能:
-
反对
echo
子命令,对输出的字符串做解决来输入- 若不提供任何选项,则输入原始内容
- 若提供
--lower
选项,则输入小写字符串 - 若提供
--upper
选项,则输入大写字符串
- 反对
eval
子命令,针对输出调用 Python 的eval
函数,将后果输入(作为示例,咱们不思考安全性问题)
argparse
argparse 作为 Python 的规范库,可能会是你想到第一个命令行库。
argparse
的设计理念就是提供给开发者最细粒度的管制。换句话说,你须要通知它必不可少的细节,比方参数的类型是什么,解决参数的动作是怎么的。
在 argparse
的世界中,须要:
- 设置解析器,作为后续定义参数和解析命令行的根底。如果要实现子命令,则还要设置子解析器。
- 定义参数,包含名称、类型、动作、帮忙等。其中的动作是指对于此参数的初步解决,是间接存下来,还是作为布尔值,亦或是追加到列表中等等
- 解析参数
- 依据参数编写业务逻辑
以下示例是基于 argparse
的 calc
程序:
`import argparse
def echo_text(args):
if args.lower:
print(args.text.lower())
elif args.upper:
print(args.text.upper())
else:
print(args.text)
def eval_expression(args):
print(eval(args.expression))
# 1. 设置解析器
parser = argparse.ArgumentParser(description=’Calculator Program.’)
subparsers = parser.add_subparsers()
# 2. 定义参数
# 2.1 echo 子命令
# echo 子解析器
echo_parser = subparsers.add_parser(
'echo', help='Echo input text in multiple forms')
# 增加地位参数 text
echo_parser.add_argument(‘text’, help=’Input text’)
# –lower/–upper 互斥,须要设置互斥组
echo_group = echo_parser.add_mutually_exclusive_group()
# 增加选项参数 –lower/–upper,这里 action 的作用就是将之变为布尔变量
echo_parser.add_argument(‘–lower’, action=’store_true’, help=’Lower input text’)
echo_parser.add_argument(‘–upper’, action=’store_true’, help=’Upper input text’)
# 设置此命令的处理函数
echo_parser.set_defaults(handle=echo_text)
# eval 子解析器
eval_parser = subparsers.add_parser(
'eval', help='Eval input expression and return result')
# 增加地位参数 expression
eval_parser.add_argument(‘expression’, help=’Expression to eval’)
# 设置此命令的处理函数
eval_parser.set_defaults(handle=eval_expression)
# 3. 解析参数
args = parser.parse_args([‘echo’, ‘–upper’, ‘Hello, World’])
print(args) # 后果:Namespace(lower=True, text=’Hello, World’, upper=False)
# args = parser.parse_args([‘eval’, ‘1+2*3’])
# print(args) # 后果:Namespace(expression=’1+2*3′)
# 4. 业务逻辑解决
args.handle(args)`
从上述示例能够看到,要实现子命令,对应地须要增加子解析器。而后最为要害的就是要定义参数,须要通过 add_argument
很明确地通知 argparse
参数长什么样,须要怎么解决:
- 它是地位参数
text
/expression
,还是选项参数--lower
/--upper
- 若是选项参数,是否互斥
- 参数的是存成什么模式,比方
action='store_true'
示意存成布尔 - 子命令的响应函数
通过 argparse
实现的整个过程是很计算机思维的,且比拟简短。其长处是灵便,所有的性能都涵盖到了;但毛病则是将定义和解决割裂,尤其在程序性能简单时会更加凌乱和不直观,难以了解和保护。
docopt
有人喜爱 argparse
这样命令式的写法,就会有人喜爱申明式的写法。而 docopt 凑巧这就是这样一个命令行库。设计它的初衷就是对于相熟命令行程序帮忙信息的开发者来说,间接通过编写帮忙信息来形容整个命令行参数定义的元信息会是更加简略快捷的形式。这种申明式的语法形容某种程度上会比过程式地定义参数来的更加简略和直观。
在 docopt
的世界中,须要:
- 定义接口形容 / 帮忙信息,这一步是它的特色和重点
- 解析参数,取得一个字典
- 依据参数编写业务逻辑
以下示例是基于 docopt
的 calc
程序:
`_# 1. 定义接口形容 / 帮忙信息_
“””Calculator Program.
Usage:
calc echo [–lower | –upper] <text>
calc eval <expression>
Commands:
echo Echo input text in multiple forms
eval Eval input expression and return result
Options:
-h –help Show help
–lower Lower input text
–upper Upper input text
“””
from docopt import docopt
def echo_text(args):
if args['--lower']:
print(args['<text>'].lower())
elif args['--upper']:
print(args['<text>'].upper())
else:
print(args['<text>'])
def eval_expression(args):
print(eval(args['<expression>']))
# 2. 解析命令行
args = docopt(__doc__, argv=[‘echo’, ‘–upper’, ‘Hello, World’])
# 后果:{‘–lower’: False, ‘–upper’: True, ‘<expression>’: None, ‘<text>’: ‘Hello, World’, ‘echo’: True, ‘eval’: False}
print(args)
# 3. 业务逻辑
if args[‘echo’]:
echo_text(args)
elif args[‘eval’]:
eval_expression(args)`
从上述示例能够看到,咱们通过文档字符串 __doc__
定义了接口形容,这和 argparse
中 一系列参数定义的行为是等价的,而后 docopt
便会依据这个元信息把命令行参数转换为一个字典。业务逻辑中就须要对这个字典进行解决。
相比于 argparse
:
- 对于较为简单的命令,命令和参数元信息的定义上
docopt
会更加简略 - 在业务逻辑的解决上,
argparse
在一些简略参数的解决上会更加便捷,且命令和处理函数之间能够不便路由(比方示例中的情景);相对来说docopt
转换为字典后就把所有解决交给业务逻辑的形式会更加简单
click
不论是 argparse
还是 docopt
,元信息的定义和解决都是割裂开的。而命令行程序实质上是定义参数并对参数进行解决,而解决参数的逻辑肯定是与所定义的参数有关联的。那可不可以用函数和装璜器来实现解决参数逻辑与定义参数的关联呢?click 正好就是以这种应用形式来设计的。
装璜器这样一个优雅的语法糖是元信息定义和解决逻辑之间的绝妙胶水,从而暗示了两者的路有关系。比照于前两个命令行库的路由实现着实优雅了不少。
在 click
的世界中:
- 通过装璜器定义命令和参数的元信息
- 应用此装璜器装璜处理函数
对,就是这么简略。
以下示例是基于 click
的 calc
程序:
`import sys
import click
sys.argv = [‘calc’, ‘echo’, ‘–upper’, ‘Hello, World’]
@click.group(help=’Calculator Program.’)
def cli():
pass
# 2. 定义参数
@cli.command(name=’echo’, help=’Echo input text in multiple forms’)
@click.argument(‘text’)
@click.option(‘–lower’, is_flag=True, help=’Lower input text’)
@click.option(‘–upper’, is_flag=True, help=’Upper input text’)
# 1. 业务逻辑
def echo_text(text, lower, upper):
if lower:
print(text.lower())
elif upper:
print(text.upper())
else:
print(text)
@cli.command(name=’eval’, help=’Eval input expression and return result’)
@click.argument(‘expression’)
def eval_expression(expression):
print(eval(expression))
cli()`
从上述示例能够看到,元信息定义和解决逻辑无缝绑定在一起,可能直观地看出对应的参数会如何解决,这个劣势在有大量参数须要解决时显得尤为突出。在处理函数中,接管到不再是像 argparse
或 docopt
中的一个蕴含所有参数的变量,而是具体的参数变量,这让解决逻辑在参数应用上也变得更加简便。
此外,click
还内置了很多实用工具和加强能力,如参数主动补全、分页反对、色彩、进度条等性能,可能无效晋升开发效率。
fire
尽管后面三个库曾经足够弱小,然而依然会有人认为不够简略。是否还有进一步简化的空间呢?如果只是定义函数,是否能让框架揣测出参数元信息呢?实践上还真是能够。
fire 用一种面向狭义对象的形式来玩转命令行,这种对象能够是类、函数、字典、列表等,它更加灵便,也更加简略。你都不须要定义参数类型,fire
会依据输出和参数默认值来主动判断,这无疑进一步简化了实现过程。
在 fire
的世界中,定义 Python 对象就够了。
以下示例是基于 fire
的 calc
程序:
`import sys
import fire
sys.argv = [‘calc’, ‘echo’, ‘”Hello, World”‘, ‘–upper’]
# 业务逻辑
# 类中有几个办法,就意味着命令行程序有几个同名命令
class Calc:
_# text 没有任何默认值,视为地位参数_
_# lower/upper 有布尔类型的默认值,视为选项参数 --lower/--upper,_
_# 且指定了为 True,不指定 False_
def echo(self, text, lower=False, upper=False):
"""Echo input text in multiple forms"""
if lower:
print(text.lower())
elif upper:
print(text.upper())
else:
print(text)
def eval(self, expression):
"""Eval input expression and return result"""
print(eval(expression))
fire.Fire(Calc)`
从下面的示例能够看出,应用 fire
足够的简略,一切都是依据约定来进行推断,包含反对哪些命令,每个命令承受的什么参数和选项。这种形式能够说是足够的 Pythonic,相比于 click
,fire
把命令行参数的定义和函数参数的定义融为了一体。通过它,咱们真的就只用关注业务逻辑。
不过简略往往也意味着对于简单需要的顾此失彼。仅仅通过默认值来推导命令行参数所能表白的状况是无限的,比方互斥选项、地位参数的类型限定都无奈通过框架来表白,而只能由业务逻辑去判断。
typer
那么该如何在放弃像 fire
这样简略实现的形式下,加强参数元信息的表达能力呢?既然默认参数的能力无限,那么如果应用 Python 3 的类型注解呢?
typer 站在 click
伟人的肩膀上,借助 Python 3 类型注解的个性,既满足了简略直观编写的须要,又达到了应答简单场景的目标,堪称是现代化的命令行库。
在 typer
的世界中,也是间接编写业务逻辑,和 fire
稍稍不同的点是应用了类型注解和默认值来表白参数元信息定义。
以下示例是基于 typer
的 calc
程序:
`import sys
import typer
sys.argv = [‘calc’, ‘echo’, ‘”Hello, World”‘, ‘–upper’]
cli = typer.Typer(help=’Calculator Program.’)
# 定义命令 echo,及处理函数
# text 无默认值,视为地位参数,类型为字符串
# lower/upper 类型为 bool,默认值为 False,视为选项 –lower/–upper,
# 且指定了为 True,不指定 False
@cli.command(name=’echo’)
def echo_text(text: str, lower: bool = False, upper: bool = False):
"""Echo input text in multiple forms"""
if lower:
print(text.lower())
elif upper:
print(text.upper())
else:
print(text)
# 定义命令 eval,及处理函数
# expression 无默认值,视为地位参数,类型为字符串
@cli.command(name=’eval’)
def eval_expression(expression: str):
"""Eval input expression and return result"""
print(eval(expression))
cli()`
从下面的示例能够看出,相比于 click
,它免去了参数元信息的繁琐定义,取而代之的是类型注解;相比于 fire
,它的元信息定义能力则大大加强,能够通过指定默认值为 typer.Option
或 typer.Argument
来进一步扩大参数和选项的语义。能够说是,typer
达到了简略与灵便的完满均衡。
横向比照
最初,咱们横向比照下 argparse
、docopt
、click
、fire
、typer
库的各项性能和特点:
argpase
docopt
click
fire
typer
应用步骤数
4 步
3 步
2 步
1 步
1 步
应用步骤数
- 设置解析器
- 定义参数
- 解析命令行
- 业务逻辑
- 定义接口形容
- 解析命令行
- 业务逻辑
- 业务逻辑
- 定义参数
- 业务逻辑
1 . 业务逻辑
选项参数
(如 --sum
)
✔
✔
✔
✔
✔
地位参数
(如 X Y
)
✔
✔
✔
✔
✔
参数默认值
✔
✘
✔
✔
✔
互斥选项
(如 --car
和 --bus
只能二选一)
✔
✔
✔
可通过第三方库反对
✘
✘
可变参数
(如指定多个 --file
)
✔
✔
✔
✔
✔
嵌套 / 父子命令
✔
✔
✔
✔
✔
工具箱
✘
✘
✔
✔
✔
链式命令调用
✘
✘
✘
✔
✘
类型束缚
✔
✘
✔
✘
✔
Python 的命令行库品种繁多、各具特色,它们并非是反复造轮子的产物,其背地的思维值得学习。联合横向比照的总结,能够抉择出合乎应用场景的库。如果几个库都合乎,那么就抉择你所偏爱的格调。
原文链接
本文为阿里云原创内容,未经容许不得转载。