本文详解如何通过 `@overload` 结合 `typevar`、`typevartuple` 和仅位置参数(`/`)在 mypy 中精准区分「单个参数调用」与「多个参数调用」,使 `foo(1)` 返回 `int`、`foo(1, 2)` 返回 `tuple[int, int]`,彻底解决 unpacked tuple 与单值语义冲突问题。
在 Python 类型检查中,为同时支持 foo(1)(返回单值)和 foo(1, 2)(返回元组)这类多态行为,仅靠 *args 是不够的——因为 *a: int 在类型层面表示「零个或多个 int」,无法向类型检查器传达「一个参数」
与「两个及以上参数」的语义差异。Mypy(及 Pyright)要求重载签名必须在调用时可静态区分,而不能依赖运行时 len(args) 判断。
正确解法是利用 *PEP 695 引入的 TypeVarTuple(`Ts)** 与 **仅位置参数语法(/`)** 构建两个互斥的重载分支:
以下是完整、经 Mypy 1.10+ 与 Pyright 严格模式验证的实现:
from typing import overload, TypeVar, TypeVarTuple
T = TypeVar('T')
T2 = TypeVar('T2')
Ts = TypeVarTuple('Ts')
@overload
def foo(a: T, /) -> T:
...
@overload
def foo(a0: T, a1: T2, /, *rest: *Ts) -> tuple[T, T2, *Ts]:
...
def foo(a: T, /, *rest: *Ts) -> T | tuple[T, *Ts]:
if len(rest) == 0:
return a
return (a, *rest)✅ 类型检查效果(Mypy/Pyright 均一致):
reveal_type(foo(1)) # Revealed type is "builtins.int" reveal_type(foo(1, 2)) # Revealed type is "tuple[builtins.int, builtins.int]" reveal_type(foo(1, 2.0, "3")) # Revealed type is "tuple[builtins.int, builtins.float, Literal['3']]"
⚠️ 关键注意事项:
? 替代方案对比:
若暂不支持 TypeVarTuple(如旧版 Mypy),可退化为固定元数重载(如 foo(a: int), foo(a: int, b: int), foo(a: int, b: int, c: int)),但会丧失对任意长度元组的支持。因此,推荐优先采用 TypeVarTuple 方案——它既保持了 *args 的灵活性,又赋予类型系统精确的结构感知能力。