如何让Python泛型函数返回类型基于可变位置参数的最后一项?
解决可变位置参数函数的最后一个参数类型标注问题
你需要实现的foo函数是一个链式调用函数,接收1个或多个可调用对象,返回类型与最后一个可调用对象的返回类型一致。要在保持所有参数为位置参数的前提下完成类型提示,可以分版本采用不同方案:
Python 3.11+:使用TypeVarTuple和Unpack实现完整可变参数类型推断
Python 3.11引入的TypeVarTuple和Unpack特性,能完美处理可变数量的类型序列,同时精准追踪链式调用的类型衔接:
from typing import TypeVar, Callable, TypeVarTuple, Unpack T0 = TypeVar("T0") T = TypeVar("T") Intermediate = TypeVarTuple("Intermediate") def foo( first: Callable[[], T0], *rest: Unpack[tuple[Callable[[T0], Intermediate[0]], Callable[[Intermediate[0]], Intermediate[1]], ..., Callable[[Intermediate[-1]], T]]] ) -> T: result = first() for fn in rest: result = fn(result) return result
这个定义让类型检查器自动追踪每一步的类型逻辑:
- 第一个函数必须是无参可调用,返回类型为
T0 - 后续每个函数都必须接收前一个函数的返回类型作为输入
- 最后一个函数的返回类型
T就是整个foo函数的返回类型
测试调用时,类型推断完全符合预期:
foo(lambda: "hello") # 推断返回类型为 str foo(lambda: "hello", str.upper) # 推断返回类型为 str foo(lambda: "hello", len) # 推断返回类型为 int
兼容Python 3.10及以下:使用@overload覆盖常用参数数量
如果需要支持更低版本的Python,只能通过@overload定义多个显式签名,覆盖常见的参数数量场景:
from typing import TypeVar, Callable, overload T = TypeVar("T") T1 = TypeVar("T1") T2 = TypeVar("T2") T3 = TypeVar("T3") @overload def foo(fn: Callable[[], T]) -> T: ... @overload def foo(fn1: Callable[[], T1], fn2: Callable[[T1], T]) -> T: ... @overload def foo(fn1: Callable[[], T1], fn2: Callable[[T1], T2], fn3: Callable[[T2], T]) -> T: ... # 可根据需求继续添加更多重载,比如4个、5个参数的情况 def foo(*fns): result = fns[0]() for fn in fns[1:]: result = fn(result) return result
这种方法的缺点是参数数量受限于你定义的重载数量,但能在低版本Python中提供准确的类型提示。
为什么最初的方案行不通?
Python语法规定,*可变位置参数之后的参数默认是关键字-only参数(PEP 3102),所以你尝试的def foo(*fns: Callable[..., Any], fnN: Callable[..., T]) -> T会强制fnN必须通过关键字传递,无法作为位置参数的最后一项,这不符合你的调用需求。
内容的提问来源于stack exchange,提问作者frans
相关产品推荐
相关产品推荐

