Python 命令行库的大乱斗(python3 命令行)
off999 2024-10-26 12:03 87 浏览 0 评论
当你想实现一个命令行程序时,或许第一个想到的是用 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 库的各项功能和特点:
Python 的命令行库种类繁多、各具特色,它们并非是重复造轮子的产物,其背后的思想值得学习。结合横向对比的总结,可以选择出符合使用场景的库。如果几个库都符合,那么就选择你所偏爱的风格。
本文为阿里云原创内容,未经允许不得转载。
相关推荐
- windows xp 死机之歌(windows死机之歌完整)
-
无法正常关机,其原因有:1、关机前运行了很多程序尚未关闭,造成关机命令延后执行或系统假死。2、关机程序与其它程序发生冲突或损坏。3、病毒感染,或者是硬盘或内存出现问题等等。处理办法:1、强制...
- win10新硬盘分区(win10硬盘分区整数对照表)
-
要对一个新硬盘进行分区,你可以按照以下步骤进行操作:1.连接新硬盘到计算机上,并确保它被正确地识别。2.打开“文件资源管理器”或者按下Win键+E来打开资源管理器窗口。3.在资源管理器左侧的导航...
- 华硕电脑官网驱动下载(华硕官网驱动下载教程)
-
在华硕官网上查找和下载驱动的步骤如下:1.打开华硕官网的支持页面:https://www.asus.com/support/2.在“输入型号名称以开始支援”搜索框中,输入你的华硕设备型号或者序列号...
- 电脑无线网络连接在哪里打开
-
1、首先打开控制面板,在开始菜单中打开。2、打开控制面板之后,找到网络和共享中心这个选项,然后打开。3、打开网络和共享中心之后,这里有详细的关于网络连接方面的信息。点击中部的设置新的连接或网络。4、如...
- 蓝屏怎么进入安全模式(电脑蓝屏怎么进入安全模式)
-
电脑蓝屏进入安全模式的方法如下:方法一:点击“开始菜单”图标,随后选择其中的“设置”图标进入。电脑会弹出设置界面,直接在搜索框中输入“恢复”进行搜索。点击“恢复”中的“高级选项”,随后选择“立即启动”...
- 能打开psd格式的软件(psd可以用什么软件打开)
-
可以打开,具体操作方法步骤如下:01手机打开medibang创建一个空白的画布。02右下角选择“图层”管理。03点击“+”加号,在菜单中选择“选择图片追加”。04选择图片会弹出窗口,调节旋转角度以及...
- office 2010下载 免费完整版
-
1、先下载office2010完整安装包免费版。2、最开始安装的时候,需要借助一款微软专用清理工具:msiclear。下载软件后双击msicuu2.exe运行软件。3、运行msicuu2.exe后...
- ide接口转sata接口线(ide接口转sata接口 bios设置)
-
ide接口转sata接口慢的很,IDE接口转SATA接口后的传输数据速率只能达到IDE接口的传输速度。ide接口:是电子集成驱动器,数据传输速度慢、线缆长度过短、连接设备少,速度100M/s。SATA...
- 网络不好怎么办(无线网网络不好怎么办)
-
调整发射功率首先,我们进入到无线路由器的管理后台,在无线设置中找到“发射功率”这一选项。由于国家规定无线路由器发射功率不得超过100mw,所以许多无线路由器在出厂时默认的发射功率为规定的50%或75%...
- win7最低配置硬盘基本要求(windows7最低硬盘要求)
-
操作系统对于硬件平台要求并不高,不要别老旧的误传给误导了。无论是windows7还是windows10都是如此。windows7系统安装时要求的最低硬盘空间分别是16GB可用硬盘空间(3...
- 笔记本锐龙处理器性能排行(最新笔记本锐龙处理器)
-
锐龙和酷睿处理器各有优势,看使用场景。1、只考虑玩游戏对于游戏玩家来说intel酷睿处理器更好一些,不是说锐龙就不好,只是对于某些主流网游,尤其腾讯系列游戏,酷睿系列更适合,因为腾讯游戏只吃单线程频率...
- 电脑声音未安装音频设备怎么办
-
这种情况属于声卡安装不正确,只需要更新声卡驱动即可解决,步骤如下:所需材料:电脑、驱动人生。一、首先打开驱动人生软件,进入主界面后点击“立即体检”。二、扫描完成后,点击声卡右边的“小箭头”。三、弹出的...
- createview教学一体机(createview教学一体机怎么开机)
-
一体机启动方法如下:1、首先打开电脑,点击“下一步”默认值不需要修改;2、点击“接受”;3、点击左下边的“跳过此步骤”;4、点击“使用快速设置”;5、在此只需要输入一个英文用户名,其他的都不需要填写...
- win10关机代码bat(windows关机命令bat)
-
要编写一个定时关机的BAT文件,你可以按照以下步骤进行操作:1.打开文本编辑器(如记事本)。2.在新建的文本文件中,输入以下命令:```@echooffecho正在关闭计算机...shutdo...
- wlan不可上网怎么回事(wlan显示不可上网怎么办回事)
-
无线网没有设置好,可以重新设置下。没有使用路由器时,电脑直接连接宽带上网,现在使用路由器共用宽带上网,则需要用路由器来直接连接宽带。根据入户宽带线路的不同,可以分为网线、电话线、光纤三种接入方式。连接...
欢迎 你 发表评论:
- 一周热门
-
-
抖音上好看的小姐姐,Python给你都下载了
-
全网最简单易懂!495页Python漫画教程,高清PDF版免费下载
-
飞牛NAS部署TVGate Docker项目,实现内网一键转发、代理、jx
-
Python 3.14 的 UUIDv6/v7/v8 上新,别再用 uuid4 () 啦!
-
python入门到脱坑 输入与输出—str()函数
-
宝塔面板如何添加免费waf防火墙?(宝塔面板开启https)
-
Python三目运算基础与进阶_python三目运算符判断三个变量
-
(新版)Python 分布式爬虫与 JS 逆向进阶实战吾爱分享
-
失业程序员复习python笔记——条件与循环
-
系统u盘安装(win11系统u盘安装)
-
- 最近发表
- 标签列表
-
- python计时 (73)
- python安装路径 (56)
- python类型转换 (93)
- python进度条 (67)
- python吧 (67)
- python的for循环 (65)
- python格式化字符串 (61)
- python静态方法 (57)
- python列表切片 (59)
- python面向对象编程 (60)
- python 代码加密 (65)
- python串口编程 (77)
- python封装 (57)
- python写入txt (66)
- python读取文件夹下所有文件 (59)
- python操作mysql数据库 (66)
- python获取列表的长度 (64)
- python接口 (63)
- python调用函数 (57)
- python多态 (60)
- python匿名函数 (59)
- python打印九九乘法表 (65)
- python赋值 (62)
- python异常 (69)
- python元祖 (57)
