如何为接受可变数量Callable参数的函数标注返回类型,使其与参数返回类型逐一匹配?
这个问题确实戳中了Python类型提示里一个挺常见的痛点——当你需要让可变参数的返回类型和参数本身的类型一一对应时,常规的标注方式确实不够用。你之前的写法只能表示返回一个由相同类型元素组成的元组,既没法绑定返回元组的长度和传入的consumer_callables数量一致,也没法区分每个callable返回不同类型的情况。
其实你提到的PEP 646就是解决这个问题的关键,只是可能没找对具体的用法。我们可以用**类型变量元组(Type Variable Tuples)**来实现你想要的类型绑定效果,具体步骤如下:
第一步:导入必要的类型工具
如果你的Python版本是3.11及以上,直接从标准库typing导入;如果是更低版本,需要先安装typing_extensions包,再从里面导入:
from typing import Iterable, Callable, Tuple, Annotated, TypeVar # Python 3.11+ 用下面的导入 # from typing import TypeVarTuple, Unpack # 低于3.11版本用这个 from typing_extensions import TypeVarTuple, Unpack
第二步:定义合适的类型变量
我们需要把原来单个的R类型变量换成类型变量元组,用来表示一组可变的返回类型(每个元素对应一个callable的返回类型):
Y = Annotated[TypeVar("Y"), "That which might be yielded"] Rs = TypeVarTuple("Rs") # 这个元组类型变量会捕获每个callable的返回类型
第三步:修改函数签名
现在我们可以用Unpack来展开类型变量元组,分别标注可变参数和返回值:
def teeinto_constantmemory( it: Iterable[Y], *consumer_callables: Callable[[Iterable[Y]], Unpack[Rs]] ) -> Tuple[Unpack[Rs]]: """将可迭代对象分流给多个消费者函数,返回每个消费者的结果组成的元组""" ...
效果演示
当你像这样调用函数时:
class FooType: pass def consume_int(it: Iterable[int]) -> int: return sum(it) def consume_float(it: Iterable[int]) -> float: return float(next(iter(it))) def consume_str(it: Iterable[int]) -> str: return ",".join(map(str, it)) def consume_foo(it: Iterable[int]) -> FooType: return FooType() # 调用函数 result = teeinto_constantmemory([1, 2, 3], consume_int, consume_float, consume_str, consume_foo)
类型检查器会正确推断result的类型为Tuple[int, float, str, FooType],完全符合你想要的“返回元组长度和参数数量一致、每个元素类型对应对应callable返回类型”的需求。
为什么原来的写法不行?
你之前用单个TypeVar定义的R,会把所有consumer_callables的返回类型约束为同一个类型,返回元组也只能是Tuple[R, ...](即所有元素都是R类型),没法处理不同callable返回不同类型的场景。而类型变量元组Rs可以捕获一组不同的类型,通过Unpack分别应用在可变参数和返回元组上,就能实现一一对应的类型绑定。
所以这个场景其实已经被Python的类型提示生态支持了,只是需要用到PEP 646引入的类型变量元组特性~
备注:内容来源于stack exchange,提问作者JamesTheAwesomeDude

