You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何为返回不同类型的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作为带参数的协议,其类型参数Dupdate方法中是逆变的(因为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}")

效果说明:

  1. 当调用factory.create(1)时,mypy会自动推断返回类型是list[BaseAlg[Data1]];调用create(2)则推断为list[BaseAlg[Data2]],完美匹配业务逻辑。
  2. 新增数据类型时,只需添加对应的@overload签名和创建方法,无需修改主返回类型注解,扩展性强。
  3. 这种方式准确表达了“传入的参数决定返回的算法操作对应数据类型”的语义,比联合类型更清晰。

三、替代方案:封装同数据类型的算法集合

如果希望更抽象地表达“一组操作相同数据的算法”,可以定义一个新协议:

class AlgCollection(Protocol[D]):
    def get_algorithms(self) -> list[BaseAlg[D]]: ...

然后让工厂方法返回AlgCollection[D],但这种方式会增加一层封装,适合更复杂的场景,简单场景下用@overload+Literal的方案更直接。

四、设计合理性说明

你的设计本身没有根本性缺陷,这是泛型类型系统中常见的“动态参数映射静态类型”问题。通过overload+Literal的方式,既保留了动态工厂的灵活性,又让静态类型检查器能正确识别类型关联,是比较优雅的解决方案。

内容的提问来源于stack exchange,提问作者Durtal

火山引擎 最新活动