如何在Python中正确标注泛型可调用对象的类型?
问题场景
我写了一段代码,想实现不同命令类和对应处理器的类型匹配,但遇到了mypy类型检查错误。
原始代码
from collections.abc import Callable, Coroutine from typing import Any, Generic, TypeVar CRT = TypeVar("CRT", bound=Any) class Command(Generic[CRT]): pass CommandHandler = Callable[[Command[CRT]], Coroutine[Any, Any, CRT]] class MyCommand(Command[int]): pass async def my_command_handler(command: MyCommand) -> int: return 42 async def process_command(command: Command[CRT], handler: CommandHandler[CRT]) -> CRT: return await handler(command) async def main() -> None: my_command = MyCommand() await process_command(my_command, my_command_handler)
报错信息
执行await process_command(my_command, my_command_handler)时,mypy抛出错误:
Argument 2 to "process_command" has incompatible type "Callable[[MyCommand], Coroutine[Any, Any, int]]"; expected "Callable[[Command[int]], Coroutine[Any, Any, int]]"
(function) def my_command_handler(command: MyCommand) -> CoroutineType[Any, Any, int]
我知道MyCommand是Command[int]的子类,但类型检查就是不通过。我的目标是让每个命令类定义自己的预期返回结果,同时给process_command添加准确的类型标注,确保处理器只能接收对应命令实例,且返回正确结果。虽然把处理器改成下面的形式能通过检查,但我不想在函数里加额外的类型判断:
async def my_command_handler(command: Command[int]) -> int: if not isinstance(command, MyCommand): raise TypeError("Expected MyCommand") return 42
原因分析
问题出在Callable类型的参数逆变特性:如果类型B是A的子类,那么Callable[[A], ...]可以被视为Callable[[B], ...]的子类,但反过来不行。你的my_command_handler只接受MyCommand类型的参数,而process_command期望的处理器能接受任意Command[int]类型的参数——这就导致了类型不兼容,因为处理器无法处理所有Command[int]的子类。
解决方案
通过给命令类型单独定义协变的TypeVar,让处理器的参数类型和传入的命令类型严格绑定,就能解决这个问题。以下是两种可行的实现方式:
方式一:用两个TypeVar分别绑定命令和返回类型
from collections.abc import Callable, Coroutine from typing import Any, Generic, TypeVar # CRT表示处理器的返回类型,CMD表示命令类型(声明为协变) CRT = TypeVar("CRT", bound=Any) CMD = TypeVar("CMD", bound="Command[CRT]", covariant=True) class Command(Generic[CRT]): pass # 让CommandHandler的参数为CMD类型,而非固定的Command[CRT] CommandHandler = Callable[[CMD], Coroutine[Any, Any, CRT]] class MyCommand(Command[int]): pass async def my_command_handler(command: MyCommand) -> int: return 42 # 修改process_command的类型标注,关联CMD和CRT async def process_command(command: CMD, handler: CommandHandler[CMD, CRT]) -> CRT: return await handler(command) async def main() -> None: my_command = MyCommand() await process_command(my_command, my_command_handler)
方式二:直接在process_command中绑定命令子类
from collections.abc import Callable, Coroutine from typing import Any, Generic, TypeVar CRT = TypeVar("CRT", bound=Any) # 声明CMD是Command[CRT]的子类 CMD = TypeVar("CMD", bound=Command[CRT]) class Command(Generic[CRT]): pass class MyCommand(Command[int]): pass async def my_command_handler(command: MyCommand) -> int: return 42 # 直接标注处理器接受CMD类型参数 async def process_command(command: CMD, handler: Callable[[CMD], Coroutine[Any, Any, CRT]]) -> CRT: return await handler(command) async def main() -> None: my_command = MyCommand() await process_command(my_command, my_command_handler)
两种方式都能让mypy通过类型检查,同时不需要在处理器内部添加额外的类型判断,完美实现命令与处理器的类型绑定。
内容的提问来源于stack exchange,提问作者niltz

