我有由数据加载器和数据转换器组成的应用程序。每个加载器和每个转换器都是抽象基本加载器和抽象基本转换器的子类,我将在下面的示例中省略它们。具体的加载器和变压器之间存在 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 抱怨:
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
是分开的,不会混合在一起?
是否有可用于此用例的替代模式?
好的,这是解决这个问题的第三次尝试。在这次尝试中,我使用抽象协议告诉 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)
在我的游乐场这里
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)
这里的问题是,您的
make_app
use_app1
是
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
的文档位于这里
;
typing.Literal
的文档位于 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)