将独立分支组合成通用结构的设计模式
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 -> Transformer1
和Loader2 -> Data2 -> Transformer2
是分开的,不会混在一起?
是否有可用于此用例的替代模式?
这里的问题是,实际上您的 make_app
函数有两种不同的签名:如果 use_app1
是 True
,则一种签名是 True
,而如果 use_app1
是,则另一种签名False
。 typing.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类型检查器。该解决方案使用通用抽象基 类 来明确类型检查器 Data1
和 Data2
具有相同的接口,等等:
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。
我有包含数据加载器和数据转换器的应用程序。每个加载器和每个转换器都是抽象基础加载器和抽象基础转换器的子类,我将在下面的示例中省略。具体的加载器和转换器之间存在 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 -> Transformer1
和Loader2 -> Data2 -> Transformer2
是分开的,不会混在一起?
是否有可用于此用例的替代模式?
这里的问题是,实际上您的 make_app
函数有两种不同的签名:如果 use_app1
是 True
,则一种签名是 True
,而如果 use_app1
是,则另一种签名False
。 typing.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类型检查器。该解决方案使用通用抽象基 类 来明确类型检查器 Data1
和 Data2
具有相同的接口,等等:
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。