click 是 Python 里最老牌的命令行框架之一,typer 就是建在它上面的。这篇不单独吹它,而是把它和 argparse、typer 三者的启动开销放在一条线上量,因为很多人选框架时只看代码好不好写,不看每次运行要多花多少时间。

同一个工具,三种写法

功能完全一样:hello 打招呼(带重复次数和大声开关),add 两数相加。click 的写法是装饰器堆叠:

@cli.command()
@click.argument("name")
@click.option("--times", default=1, type=int)
@click.option("--loud", is_flag=True)
def hello(name, times, loud):
    """打招呼"""

参数一个个声明,但每个声明自带帮助文本和类型校验。is_flag=True 让布尔值变成开关,type=int 让传错时报错。

对比 argparse 要写的 sub.add_parser() 加三条 add_argument() 加分发逻辑,click 用装饰器把「声明」和「函数」绑在了一起,少一层手工映射。

三个框架的行数与启动耗时

框架代码行数启动(--help 中位)最快 / 最慢
argparse25745 毫秒739 / 826
click25931 毫秒840 / 970
typer181300 毫秒1265 / 1495

三个数字连起来看很有意思:

click 比 argparse 多 186 毫秒。同样的行数,同样的功能,差别全在 import:click 有自己的解析器、上下文栈、参数类型系统,这些模块在进程启动时都要加载。

typer 比 click 再多 369 毫秒。因为 typer 不仅 import click,还会拉 rich 来渲染那个带边框的帮助面板。369 毫秒就是这块渲染能力的代价。

从 argparse 换到 typer,启动成本翻了 1.75 倍。

click 换来了什么

命令行分组和嵌套子命令。argparse 的 subparser 只能一层,click 的 @click.group() 可以嵌套多层,做那种 tool db migrate 的三级命令很自然。

参数类型系统。@click.argument("a", type=int) 之外还有 click.Path(exists=True)、click.Choice([...])、click.File("r"),这些类型自带校验和错误提示,argparse 要自己写。

上下文对象。@click.pass_context 能在父子命令之间传状态,做那种「全局参数在子命令里也要用」的场景比 argparse 干净。

测试友好。click 自带 CliRunner,可以在测试里直接调命令、断言输出和退出码,不用起子进程。argparse 要测只能手动构造 argv。

它在生态里的位置

很多你正在用的工具底层都是 click:Flask 的命令行、pip 的部分子命令、黑化工具 black、数据库迁移工具 alembic。它的稳定性经得起推敲,从 2014 年到现在接口基本没大改。

typer 是它的封装层,用类型注解替代装饰器参数声明。所以学习路径上,先懂 click 再看 typer 会轻松很多,反过来则容易只记住语法不懂原理。

三个框架怎么选

脚本只在部署时跑一次、或者被 cron 调用,启动开销无所谓,按代码好写程度选。typer 最省行数,18 行对 25 行。

命令要被高频调用(编辑器插件、git 钩子、shell 补全的每次 Tab),那 186 毫秒和 369 毫秒都会被放大。这种情况 argparse 最稳,标准库零依赖。

中间地带选 click:比 argparse 好写,比 typer 快 369 毫秒,而且帮助面板虽然朴素但中文对齐没问题(它按显示宽度算,不是按字符数)。

还有一点容易被忽略:如果你把工具用 PyInstaller 打成 exe,import 的那部分开销会体现在每次启动上,实测过一次 onefile 模式的 exe 启动要 840 毫秒,再叠加框架的 import 成本会到 1.5 秒以上。这种场景下框架选择比代码行数重要得多。

没测的部分列一下,3 项:多层嵌套命令的实际代码量对比 / CliRunner 在测试里的耗时 / 打成 exe 后的启动开销。