如何为返回不同类型的Python工厂方法添加类型提示?
我正在开发一个通用框架,用于解决不同但相关的问题。每个问题包含数据和一组对该数据操作的算法,数据和算法因问题而异,因此需要不同的类,但它们都共享通用接口。
我从定义问题的配置文件开始。在程序的某个环节,我需要一个根据参数值(而非类型)返回不同类实例的函数/方法。
相关代码如下:
from dataclasses import dataclass from typing import Protocol # Protocols class BaseData(Protocol): common: int class BaseAlg[D: BaseData](Protocol): def update(self, data: D) -> None: ... # Implementations data @dataclass class Data1: common: int extra: int @dataclass class Data2: common: int extra: str # Implementations algorithms class Alg1: def update(self, data: Data1) -> None: data.extra += data.common class Alg2a: def update(self, data: Data2) -> None: data.extra *= data.common class Alg2b: def update(self, data: Data2) -> None: data.extra += "2b"
现在我需要一个工厂来初始化每个问题对应的算法(数据部分已省略):
class FactoryAlgorithms: def _create_1(self) -> list[BaseAlg[Data1]]: return [Alg1()] def _create_2(self) -> list[BaseAlg[Data2]]: return [Alg2a(), Alg2b()] def create(self, type_alg: int): # <- How to annotate the return type? match type_alg: case 1: return self._create_1() case 2: return self._create_2() case _: raise ValueError(f"Unknown type of data {type_alg}")
核心疑问
如何为泛型的create方法注解返回类型?
mypy接受list[BaseAlg[Data1]] | list[BaseAlg[Data2]],但存在两个问题:
- 随着业务逻辑(算法和数据结构)增多,这种方式会变得繁琐。
- 这种显式类型并未真正反映我想要返回的内容:一组均操作相同数据的算法。
我直觉上会写list[BaseAlg[BaseData]],但被mypy拒绝,推测是协变/逆变的原因:
Incompatible return value type (got "list[BaseAlg[Data1]]", expected "list[BaseAlg[BaseData]]")
有没有办法用泛型解决这个问题?还是这个设计存在根本性缺陷?
一、理解类型不兼容的根源
首先明确为什么list[BaseAlg[Data1]]不能赋值给list[BaseAlg[BaseData]]:
BaseAlg作为带参数的协议,其类型参数D在update方法中是逆变的(因为D是方法的输入参数)。- 逆变意味着
BaseAlg[Data1]是BaseAlg[BaseData]的父类型,而非子类型——反过来才成立。而列表是不变类型,所以list[Parent]和list[Child]之间不存在兼容关系。
二、用泛型+TypeVar+Overload解决返回类型问题
可以通过定义带约束的TypeVar,结合Literal和@overload来让mypy识别参数和返回类型的关联,同时保持代码的扩展性:
from dataclasses import dataclass from typing import Protocol, TypeVar, Literal, overload # 定义带约束的TypeVar D = TypeVar("D", bound=BaseData) # Protocols class BaseData(Protocol): common: int class BaseAlg(Protocol[D]): def update(self, data: D) -> None: ... # 数据实现部分不变 @dataclass class Data1: common: int extra: int @dataclass class Data2: common: int extra: str # 算法实现部分不变 class Alg1: def update(self, data: Data1) -> None: data.extra += data.common class Alg2a: def update(self, data: Data2) -> None: data.extra *= data.common class Alg2b: def update(self, data: Data2) -> None: data.extra += "2b" class FactoryAlgorithms: def _create_1(self) -> list[BaseAlg[Data1]]: return [Alg1()] def _create_2(self) -> list[BaseAlg[Data2]]: return [Alg2a(), Alg2b()] # 使用@overload为不同参数值定义明确的返回类型 @overload def create(self, type_alg: Literal[1]) -> list[BaseAlg[Data1]]: ... @overload def create(self, type_alg: Literal[2]) -> list[BaseAlg[Data2]]: ... # 实际实现的类型注解用TypeVar def create(self, type_alg: int) -> list[BaseAlg[D]]: match type_alg: case 1: return self._create_1() case 2: return self._create_2() case _: raise ValueError(f"Unknown type of data {type_alg}")
效果说明:
- 当调用
factory.create(1)时,mypy会自动推断返回类型是list[BaseAlg[Data1]];调用create(2)则推断为list[BaseAlg[Data2]],完美匹配业务逻辑。 - 新增数据类型时,只需添加对应的
@overload签名和创建方法,无需修改主返回类型注解,扩展性强。 - 这种方式准确表达了“传入的参数决定返回的算法操作对应数据类型”的语义,比联合类型更清晰。
三、替代方案:封装同数据类型的算法集合
如果希望更抽象地表达“一组操作相同数据的算法”,可以定义一个新协议:
class AlgCollection(Protocol[D]): def get_algorithms(self) -> list[BaseAlg[D]]: ...
然后让工厂方法返回AlgCollection[D],但这种方式会增加一层封装,适合更复杂的场景,简单场景下用@overload+Literal的方案更直接。
四、设计合理性说明
你的设计本身没有根本性缺陷,这是泛型类型系统中常见的“动态参数映射静态类型”问题。通过overload+Literal的方式,既保留了动态工厂的灵活性,又让静态类型检查器能正确识别类型关联,是比较优雅的解决方案。
内容的提问来源于stack exchange,提问作者Durtal




