将独立分支组合成通用结构的设计模式

Design pattern for combining separate branches into common structure

我有包含数据加载器和数据转换器的应用程序。每个加载器和每个转换器都是抽象基础加载器和抽象基础转换器的子类,我将在下面的示例中省略。具体的加载器和转换器之间存在 1:1 映射,即知道哪个加载器和转换器属于一起。

假设我们有两个加载器和两个转换器来处理数据

class Data1: ...

class Data2: ...


class Loader1:
    def get_data(self) -> Data1: ...

class Loader2:
    def get_data(self) -> Data2: ...


class Transformer1:
    def transform_data(self, data: Data1) -> None: ...

class Transformer2:
    def transform_data(self, data: Data2) -> None: ...

现在可以将这些 类 合并到应用程序中

class App1:
    Loader = Loader1
    Transformer = Transformer1

class App2:
    Loader = Loader2
    Transformer = Transformer2

有配套工厂

from typing import Union, Type

def make_app(use_app1: bool) -> Union[Type[App1], Type[App2]]:
    if use_app1:
        return App1
    else:
        return App2

这就是我想使用上面的方法

def main(use_app1: bool) -> None:
    app = make_app(use_app1)
    loader = app.Loader()
    data = loader.get_data()
    transformer = app.Transformer()
    transformer.transform_data(data=data)

然而,mypy complains:

error: Argument "data" to "transform_data" of "Transformer1" has incompatible type "Union[Data1, Data2]"; expected "Data1"  [arg-type]
error: Argument "data" to "transform_data" of "Transformer2" has incompatible type "Union[Data1, Data2]"; expected "Data2"  [arg-type]

有没有办法让mypy相信分支Loader1 -> Data1 -> Transformer1Loader2 -> Data2 -> Transformer2是分开的,不会混在一起?

是否有可用于此用例的替代模式?

这里的问题是,实际上您的 make_app 函数有两种不同的签名:如果 use_app1True,则一种签名是 True,而如果 use_app1 是,则另一种签名Falsetyping.overload 结合 typing.Literal 是这里的解决方案,因为 @overload 允许我们注册一个函数的多个不同签名。用 @overload 装饰的函数的实现在运行时会被忽略——它们只用于类型检查器——所以这些函数的主体可以留空。通常,您只需在这些函数的主体中放置文字省略号 ... 或文档字符串。必须至少有一个未用 @overload 修饰的函数的具体实现,以便在运行时使用。

from typing import overload, Literal, Union, Type

App1Type = Type[App1]
App2Type = Type[App2]

@overload
def make_app(use_app1: Literal[True]) -> App1Type:
    """Signature of the function when `use_app1` is True"""

@overload
def make_app(use_app1: Literal[False]) -> App2Type:
    """Signature of the function when `use_app1` is False"""

def make_app(use_app1: bool) -> Union[App1Type, App2Type]:
    """Concrete implementation of the function, for use at runtime"""

    if use_app1:
        return App1
    else:
        return App2

typing.overload 的文档是 here; the documentation for typing.Literal is here


编辑:看起来这行不通。您可以通过像这样重写 main 函数来使其工作,但肯定有比做一个真正毫无意义的 if-else 语句更好的方法...

def main(use_app1: Literal[True, False]) -> None:
    app: Union[App1Type, App2Type]
    loader: Union[Loader1, Loader2]
    data: Union[Data1, Data2]
    transformer: Union[Transformer1, Transformer2]
    
    if use_app1:
        app = make_app(use_app1)
        loader = app.Loader()
        data = loader.get_data()
        transformer = app.Transformer()
        transformer.transform_data(data=data)
    else:
        app = make_app(use_app1)
        loader = app.Loader()
        data = loader.get_data()
        transformer = app.Transformer()
        transformer.transform_data(data=data)

这是一个不同的答案,它仍然涉及大量的额外代码,但至少比在其中添加一个毫无意义的 if-else 语句更 sense类型检查器。该解决方案使用通用抽象基 类 来明确类型检查器 Data1Data2 具有相同的接口,等等:

from abc import abstractmethod, ABCMeta
from typing import Union, Type, TypeVar, Any, Protocol


### ABSTRACT INTERFACES ###


class AbstractData:
    __slots__ = ()
    

class AbstractLoader(metaclass=ABCMeta):
    __slots__ = ()
    
    @abstractmethod
    def get_data(self) -> AbstractData: ...
    

D = TypeVar('D', bound=AbstractData, contravariant=True)
    

class AbstractTransformer(Protocol[D]):
    __slots__ = ()
    
    @abstractmethod
    def transform_data(self, data: D) -> None: ...


L = TypeVar('L', bound=AbstractLoader, covariant=True)
T = TypeVar('T', bound=AbstractTransformer[Any], covariant=True)


class AbstractApp(Protocol[L, T]):
    __slots__ = ()
    
    @classmethod
    @property
    @abstractmethod
    def Loader(cls) -> Type[L]: ...
    
    @classmethod
    @property
    @abstractmethod
    def Transformer(cls) -> Type[T]: ...
    

### CONCRETE IMPLEMENTATIONS ###
    

class Data1(AbstractData): ...


class Data2(AbstractData): ...


class Loader1(AbstractLoader):
    def get_data(self) -> Data1: ...


class Loader2(AbstractLoader):
    def get_data(self) -> Data2: ...


class Transformer1(AbstractTransformer[Data1]):
    def transform_data(self, data: Data1) -> None: ...


class Transformer2(AbstractTransformer[Data2]):
    def transform_data(self, data: Data2) -> None: ...


class App1(AbstractApp[Loader1, Transformer1]):
    Loader = Loader1
    Transformer = Transformer1


class App2(AbstractApp[Loader2, Transformer2]):
    Loader = Loader2
    Transformer = Transformer2
    

def make_app(use_app1: bool) -> Type[AbstractApp[Any, Any]]:
    if use_app1:
        return App1
    else:
        return App2


def main(use_app1: bool) -> None:
    app = make_app(use_app1)
    loader = app.Loader()
    data = loader.get_data()
    transformer = app.Transformer()
    transformer.transform_data(data=data)

好的,这是解决此问题的第三次尝试。在这次尝试中,我使用抽象协议告诉 MyPy,事实上,在很多这些函数中,重要返回什么具体类型,只要返回的对象有特定的接口。

from typing import Type, Protocol, cast


### ABSTRACT INTERFACES ###


class DataProto(Protocol): ...
    

class LoaderProto(Protocol):
    def get_data(self) -> DataProto: ...
    

class TransformerProto(Protocol):
    def transform_data(self, data: DataProto) -> None: ...


class AppProto(Protocol):
    Loader: Type[LoaderProto]
    Transformer: Type[TransformerProto]
    

### CONCRETE IMPLEMENTATIONS ###
    

class Data1: ...


class Data2: ...


class Loader1:
    def get_data(self) -> Data1: ...


class Loader2:
    def get_data(self) -> Data2: ...


class Transformer1:
    def transform_data(self, data: Data1) -> None: ...


class Transformer2:
    def transform_data(self, data: Data2) -> None: ...


class App1:
    Loader = Loader1
    Transformer = Transformer1


class App2:
    Loader = Loader2
    Transformer = Transformer2
    

GenericAppClassType = Type[AppProto]
    

def make_app(use_app1: bool) -> GenericAppClassType:
    if use_app1:
        return cast(GenericAppClassType, App1)
    else:
        return cast(GenericAppClassType, App2)


def main(use_app1: bool) -> None:
    app = make_app(use_app1)
    loader = app.Loader()
    data = loader.get_data()
    transformer = app.Transformer()
    transformer.transform_data(data=data)

在 mypy playground 上试试看 here