凌云时刻 · 技术

导读:一千个码农,就有一千个库。

作者 | 王斌鑫(闲炎)

来源 | 凌云时刻(微信号:linuxpk)

前言

当你想实现一个命令行程序时,或许第一个想到的是用 Python 来实现。比如 CentOS 上大名鼎鼎的包管理工具 yum 就是基于 Python 实现的。而 Python 的世界中有很多命令行库,每个库都各具特色:

  • argparse 作为 Python 的亲儿子,意味着开箱即用,它灵活,但冗长

  • docopt 小巧灵动,与其他库风格迥异,几行文本便可定义命令行元信息

  • click 作为 flask 团队的产物,继承了使用装饰器路由的精髓,使得元信息的定义和处理变得尤为清晰

  • fire 紧抱谷歌大腿,把面向对象和 Pythonic 发挥到了极致

  • typer 作为 fastapi 的副产品,基于 click 并使用类型注解的特性,成为了命令行库的集大成者

对于这些库,我们往往不知道其背后的设计理念,也因此在选择时感到迷茫。这些库的作者为何在重复造轮子,他是从哪个角度来考虑,来让命令行库“演变”到一个新的更好用的形态。

为了能够更加直观地感受到命令行库的设计理念,在此之前,我们不妨设计一个名为 calc 的命令行程序,它能:

  • 支持 echo 子命令,对输入的字符串做处理来输出

    • 若不提供任何选项,则输出原始内容

    • 若提供 --lower 选项,则输出小写字符串

    • 若提供 --upper 选项,则输出大写字符串

  • 支持 eval 子命令,针对输入调用 Python 的 eval 函数,将结果输出(作为示例,我们不考虑安全性问题)

argparse

argparse 作为 Python 的标准库,可能会是你想到第一个命令行库。

argparse 的设计理念就是提供给开发者最细粒度的控制。换句话说,你需要告诉它必不可少的细节,比如参数的类型是什么,处理参数的动作是怎样的。

在 argparse的世界中,需要:

  • 设置解析器,作为后续定义参数和解析命令行的基础。如果要实现子命令,则还要设置子解析器。

  • 定义参数,包括名称、类型、动作、帮助等。其中的动作是指对于此参数的初步处理,是直接存下来,还是作为布尔值,亦或是追加到列表中等等

  • 解析参数

  • 根据参数编写业务逻辑

以下示例是基于 argparse 的 calc程序:

import argparsedef 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 formseval          Eval input expression and return resultOptions:-h --help     Show help--lower       Lower input text--upper       Upper input text
"""
from docopt import docoptdef 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 clicksys.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 firesys.argv = ['calc', 'echo', '"Hello, World"', '--upper']# 业务逻辑
# 类中有几个方法,就意味着命令行程序有几个同名命令
class Calc:# text 没有任何默认值,视为位置参数# lower/upper 有布尔类型的默认值,视为选项参数 --lower/--upper,# 且指定了为 True,不指定 Falsedef 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,相比于 clickfire 把命令行参数的定义和函数参数的定义融为了一体。通过它,我们真的就只用关注业务逻辑。

不过简单往往也意味着对于复杂需求的捉襟见肘。仅仅通过默认值来推导命令行参数所能表达的情况是有限的,比如互斥选项、位置参数的类型限定都无法通过框架来表达,而只能由业务逻辑去判断。

typer

那么该如何在保持像 fire 这样简单实现的方式下,增强参数元信息的表达能力呢?既然默认参数的能力有限,那么如果使用 Python 3 的类型注解呢?

typer 站在 click 巨人的肩膀上,借助 Python 3 类型注解的特性,既满足了简单直观编写的需要,又达到了应对复杂场景的目的,可谓是现代化的命令行库。

在 typer 的世界中,也是直接编写业务逻辑,和 fire 稍稍不同的点是使用了类型注解和默认值来表达参数元信息定义。

以下示例是基于 typer 的 calc 程序:

import sys
import typersys.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 达到了简单与灵活的完美平衡。

横向对比

最后,我们横向对比下 argparsedocoptclickfiretyper 库的各项功能和特点:

argpase docopt click fire typer
使用步骤数 4 步 3 步 2 步 1 步 1 步
使用步骤数 1. 设置解析器
2. 定义参数
3. 解析命令行
4. 业务逻辑
1. 定义接口描述
2. 解析命令行
3. 业务逻辑
1. 业务逻辑
2. 定义参数
1. 业务逻辑 1 . 业务逻辑
选项参数
(如 --sum
位置参数
(如 X Y
参数默认值
互斥选项
(如 --car 和 --bus 只能二选一)

可通过第三方库支持
可变参数
(如指定多个 --file
嵌套/父子命令
工具箱
链式命令调用
类型约束

Python 的命令行库种类繁多、各具特色,它们并非是重复造轮子的产物,其背后的思想值得学习。结合横向对比的总结,可以选择出符合使用场景的库。如果几个库都符合,那么就选择你所偏爱的风格。

END

往期精彩文章回顾

乘风破浪的中国数据库

SpaceX上天容易,Tesla自动驾驶难

与生命赛跑,Serverless提升AI诊断效率90%

技术创造新商业:云研发时代的效能挑战

生不出孩子怪天气?驳《我国工业软件失去的30年》一文

应云而生,原力觉醒——解读云原生基础设施

又一次全球第一!

帮您管好云:阿里云混合云管理平台发布

阿里云蒋江伟:我们致力于为世界提供70%的算力

搭载敏捷飞天底座 阿里云专有云敏捷版全面升级

长按扫描二维码关注凌云时刻

每日收获前沿技术与科技洞见

Python 命令行库的大乱斗 | 凌云时刻相关推荐

  1. Python 命令行库的大乱

    当你想实现一个命令行程序时,或许第一个想到的是用 Python 来实现.比如 CentOS 上大名鼎鼎的包管理工具 yum 就是基于 Python 实现的. 而 Python 的世界中有很多命令行库, ...

  2. Google 开源的 Python 命令行库:fire 实现 git 命令

    一.前言 在前面三篇介绍 fire 的文章中,我们全面了解了 fire 强大而不失简洁的能力.按照惯例,我们要像使用 argparse.docopt 和 click 一样使用 fire 来实现 git ...

  3. python命令行大全-用什么库写 Python 命令行程序(示例代码详解)

    一.前言 在近半年的 Python 命令行旅程中,我们依次学习了 argparse . docopt . click 和 fire 库的特点和用法,逐步了解到 Python 命令行库的设计哲学与演变. ...

  4. python 代码命令大全-用什么库写 Python 命令行程序(示例代码详解)

    一.前言 在近半年的 Python 命令行旅程中,我们依次学习了 argparse . docopt . click 和 fire 库的特点和用法,逐步了解到 Python 命令行库的设计哲学与演变. ...

  5. 你还在纠结用什么库写 Python 命令行程序?看这一篇就够了

    一.前言 在近半年的 Python 命令行旅程中,我们依次学习了 argparse.docopt.click 和 fire 库的特点和用法,逐步了解到 Python 命令行库的设计哲学与演变. 本文作 ...

  6. python用什么敲代码_你还在纠结用什么库写 Python 命令行程序?看这一篇就够了...

    一.前言 在近半年的 Python 命令行旅程中,我们依次学习了 argparse.docopt.click 和 fire 库的特点和用法,逐步了解到 Python 命令行库的设计哲学与演变. 本文作 ...

  7. Python命令行可视化库

    我们通常都是在自己的电脑上跑程序,直接是可以可视化相应的结果.如果是在服务器上的话,使用终端,是不太方便查看结果.本文介绍4个可以在命令行中使用的Python库,分别是 Bashplotlib tqd ...

  8. python 基础命令-Python 命令行(CLI)基础库

    在 CLI 下写 UI 应用 前阵子看了一下自己去年写的 Python-视频转字符动画,感觉好糗..所以几乎把整篇文章重写了一遍.并使用 curses 库实现字符动画的播放. 但是感觉,curses ...

  9. 发现 4 个 Python 命令行可视化库,又酷又炫!

    通常大家都是在自己的电脑上跑程序,直接是可以可视化相应的结果. 如果是在服务器上的话,使用终端,是不太方便查看结果. 今天给大家介绍4个可以在命令行中使用的Python库. 分别是Bashplotli ...

  10. [转载] python docopt_比较Python命令行解析库– Argparse,Docopt和Click

    参考链接: 使用Python的argparse创建命令行实用程序 python docopt About a year ago I began a job where building command ...

最新文章

  1. 简单的 H5 视频推流解决方案
  2. linux下基于内存分析的rootkit检测方法
  3. SAP LSMW批导数据的几个注意点
  4. Python基础教程:super()方法详解
  5. 基于实时计算Flink版的场景解决方案demo
  6. tensorflow版本及其对应环境
  7. java match parent_java.lang.IllegalStateException: The specified child already has a parent
  8. [转载] Python中 hash去重
  9. 求助动态贝叶斯网络参数学习函数的使用方法
  10. 在SQL server 2008 R2进行数据查询操作时提示 “对象名无效”的问题
  11. pci_register_driver
  12. oracle 获取日期的毫秒_Oracle date timestamp 毫秒 - 时间函数总结(转)
  13. 咸鱼Maya笔记—Maya 热键
  14. 2020-03-19
  15. 开学季适合学生党的蓝牙耳机,音质好的蓝牙耳机排行
  16. UKF-协方差矩阵分解
  17. 汽车控制系统matlab,汽车控制系统建模与仿真(肖仁鑫)-中国大学mooc-题库零氪
  18. 开放流程自动化标准测试实验室花开遍地,全球巡礼—上篇
  19. https://blog.csdn.net/codezjx/article/details/8872090
  20. android 碎屏功能,如何从碎屏的Android手机中恢复数据

热门文章

  1. 获取外键关联的实体对象
  2. maven 项目在 tomcat 中启动报错:Caused by: java.util.zip.ZipException: invalid LOC header (bad signature)...
  3. # 8.19考试总结
  4. restfulframework详解
  5. PowerDesigner数据模型(CDM—PDM)
  6. html中meta的设置
  7. 【转】 解决IllegalStateException: Can not perform this action after onSaveInstanceState
  8. day3-python的基础类源码解析——collection类
  9. 解决IIS进程回收后第一次访问慢的问题
  10. 豆瓣关于计算机视觉的书评及介绍