用Typer代替Argparse

Contents

最近从Argparse和python-fire迁移到Typer,Typer在某些方面比Argparse更抽象。这篇文章总结了Typer的核心概念和使用技巧。

Typer和Argparse核心差异 #

Typer的核心理念是函数签名即CLI接口,大部分情况下只需写普通Python函数,Typer会自动处理CLI部分。

Argparse思维模式 #

python
# argparse: 显式定义每个参数
parser = argparse.ArgumentParser()
parser.add_argument('--name', type=str, help='Your name')
parser.add_argument('--count', type=int, default=1)
args = parser.parse_args()

Typer思维模式 #

python
# typer: 从函数签名自动推导
import typer

def main(name: str, count: int = 1):
    print(f"Hello {name}! " * count)

if __name__ == "__main__":
    typer.run(main)

快速精通Typer #

一个最简单例子 #

python
import typer

# 最简单的 CLI
def main(name: str):
    print(f"Hello {name}")

typer.run(main)

理解参数类型映射 #

python
# Typer 自动转换类型
def main(
    name: str,                    # 必需参数
    age: int = 18,                # 可选参数,默认值
    verbose: bool = False,        # --verbose 或 --no-verbose
    items: list[str] = None       # 可多次使用 --items a --items b
):
    pass

Option和Argument #

python
from typer import Argument, Option

def main(
    # Argument: 位置参数(不需要 --)
    filename: str = Argument(..., help="Input file"),

    # Option: 选项参数(需要 --)
    output: str = Option("output.txt", "--output", "-o", help="Output file")
):
    pass

子命令模式 #

python
app = typer.Typer()

@app.command()
def add(x: int, y: int):
    """Add two numbers"""
    print(x + y)

@app.command()
def multiply(x: int, y: int):
    """Multiply two numbers"""
    print(x * y)

if __name__ == "__main__":
    app()

从Argparse迁移到Typer #

Argparse版本 #

python
parser = argparse.ArgumentParser()
parser.add_argument('input_file')
parser.add_argument('--output', '-o', default='output.txt')
parser.add_argument('--verbose', '-v', action='store_true')
parser.add_argument('--count', type=int, default=1)

Typer等效版本 #

python
def main(
    input_file: str = typer.Argument(...),
    output: str = typer.Option('output.txt', '--output', '-o'),
    verbose: bool = typer.Option(False, '--verbose', '-v'),
    count: int = typer.Option(1, '--count')
):
    pass

常见问题和解决方案 #

省略号 的含义 #

python
# ... 表示必需参数(无默认值)
name: str = typer.Argument(...)  # 必需
name: str = typer.Argument("default")  # 可选,有默认值

bool类型的特殊处理 #

python
# bool 会自动创建 --flag/--no-flag
def main(verbose: bool = False):
    # 使用: --verbose 或 --no-verbose
    pass

回调函数和验证 #

python
def validate_age(value: int):
    if value < 0:
        raise typer.BadParameter("Age must be positive")
    return value

def main(age: int = typer.Option(..., callback=validate_age)):
    pass

额外开销 #

Typer在启动时有额外的开销,主要来自:

  1. 类型注解的运行时解析

  2. Click框架的装饰器链

  3. Rich库的初始化(如果使用)

对于简单脚本,argparse的启动速度更快。但对于复杂应用,Typer的开销可以忽略不计。

参考资料 #