与超类型不兼容的签名 - abstractmethods vs Liskov for enforcing naming
incompatible signature with supertype - abstractmethods vs Liskov for enforcing naming
我有一个 ABC class,我从中得出一些 children。
children 中的每一个都具有相同的总体结构和相同的方法,但这些方法需要采用不同的参数(同一类型模型的不同变体)。我还想对这些参数进行类型检查,并确保命名以避免任何愚蠢的实现错误。
例如,我有几个记分卡模型要实现。它们都具有相同的基本结构(接受一些参数,做一些事情,return 一个整数分数)。每个 child 的 score
方法采用一组不同的特定于该个体模型的输入。
from abc import ABC, abstractmethod
class BaseScorecard(ABC):
@abstractmethod
def score(self):
"""
abstractmethod to enforce consistent naming to enforce consistent naming
for sanity and automatic doc-building
"""
return NotImplementedError
def do_common_thing(self)
"""Some complicated common logic that having a parent class helps with"""
...
class Scorecard1(BaseScorecard):
"""A specific scorecard"""
def score(
self,
var1: int,
var2: int,
var3: str,
) -> int:
"""
score method for this scorecard. Takes some arguments
specific to this one and does some complicated logic.
"""
...
class Scorecard2(BaseScorecard):
"""A different scorecard for a different population"""
def score(
self,
var1: str,
var2: float,
) -> int:
"""
A different model that takes only 2 arguments, and of different types.
"""
...
我显然希望使用相同的方法命名,以便 a) 命名在整个项目中保持一致(不同的程序员不会使用细微不同的名称),并且 b) 因为有从模块构建的自动生成,如果我注册 Scorecard1
那么脚本期望找到一个 score
方法来记录。
当然这实际上运行良好,但 mypy 会给出类似 test.py:12: error: Signature of "score" incompatible with supertype "BaseScorecard"
.
的错误
现在,我明白这违反了 Liskov。理论上,您应该能够使用子类型的参数调用超类型方法,但在这种情况下 child 参数违反了这一点。
在这种情况下,我真的不在乎,永远不要单独调用 parent 方法,包括通过 super()
,我真正想要的是强制执行一致的方法命名,但也能够在 child 方法中键入参数。
我在其他问题或 mypy 问题中看到了一些类似的东西,但答案总是只是“违反 Liskov,不要那样做,做其他事情”,这是真实的但实际上并没有多大帮助。我从来没有真正找到任何关于尝试什么的建议。
我应该使用更好的模式来执行我想要的吗?
任何帮助将不胜感激:)
您可以使 BaseScorecard
泛化它接收到的参数。由于不支持可变泛型,您可以将参数包装在 @dataclass
.
中
旁注:
- 您想将基础
score
注释为返回 int
而不是不在其中放置任何内容(默认为 Any
)。
- 您想
raise
NotImplementedError
而不是 return
它。
T = TypeVar("T")
class BaseScorecard(ABC, Generic[T]):
@abstractmethod
def score(self, args: T) -> int:
raise NotImplementedError
@dataclass
class Scorecard1Args:
var1: int
var2: int
var3: str
class Scorecard1(BaseScorecard[Scorecard1Args]):
def score(self, args: Scorecard1Args) -> int:
...
class Scorecard2(BaseScorecard[None]):
def score(self, _: None) -> int:
...
Scorecard1
然后有效地接受 var1
、var2
和 var3
,并且会像这样调用:
var1 = 1
var2 = 2
var3 = "abc"
args = Scorecard1Args(var1, var2, var3)
Scorecard1().score(args)
Scorecard2
实际上没有 args(也许它输出一个恒定的分数),但为了使其符合 BaseScoreboard
接口,我们让它采用 None
,它会像这样称呼:
Scorecard2().score(None)
如果您只想强制执行一致的命名,那么这可能有点矫枉过正。但是使用 Generic
的好处是,如果 X
相同,现在您可以认为 BaseScorecard[X]
的两个子类是等价的,而如果您在基本签名中使用 *args, **kwargs
,则会丢失类型安全。
我有一个 ABC class,我从中得出一些 children。 children 中的每一个都具有相同的总体结构和相同的方法,但这些方法需要采用不同的参数(同一类型模型的不同变体)。我还想对这些参数进行类型检查,并确保命名以避免任何愚蠢的实现错误。
例如,我有几个记分卡模型要实现。它们都具有相同的基本结构(接受一些参数,做一些事情,return 一个整数分数)。每个 child 的 score
方法采用一组不同的特定于该个体模型的输入。
from abc import ABC, abstractmethod
class BaseScorecard(ABC):
@abstractmethod
def score(self):
"""
abstractmethod to enforce consistent naming to enforce consistent naming
for sanity and automatic doc-building
"""
return NotImplementedError
def do_common_thing(self)
"""Some complicated common logic that having a parent class helps with"""
...
class Scorecard1(BaseScorecard):
"""A specific scorecard"""
def score(
self,
var1: int,
var2: int,
var3: str,
) -> int:
"""
score method for this scorecard. Takes some arguments
specific to this one and does some complicated logic.
"""
...
class Scorecard2(BaseScorecard):
"""A different scorecard for a different population"""
def score(
self,
var1: str,
var2: float,
) -> int:
"""
A different model that takes only 2 arguments, and of different types.
"""
...
我显然希望使用相同的方法命名,以便 a) 命名在整个项目中保持一致(不同的程序员不会使用细微不同的名称),并且 b) 因为有从模块构建的自动生成,如果我注册 Scorecard1
那么脚本期望找到一个 score
方法来记录。
当然这实际上运行良好,但 mypy 会给出类似 test.py:12: error: Signature of "score" incompatible with supertype "BaseScorecard"
.
现在,我明白这违反了 Liskov。理论上,您应该能够使用子类型的参数调用超类型方法,但在这种情况下 child 参数违反了这一点。
在这种情况下,我真的不在乎,永远不要单独调用 parent 方法,包括通过 super()
,我真正想要的是强制执行一致的方法命名,但也能够在 child 方法中键入参数。
我在其他问题或 mypy 问题中看到了一些类似的东西,但答案总是只是“违反 Liskov,不要那样做,做其他事情”,这是真实的但实际上并没有多大帮助。我从来没有真正找到任何关于尝试什么的建议。
我应该使用更好的模式来执行我想要的吗?
任何帮助将不胜感激:)
您可以使 BaseScorecard
泛化它接收到的参数。由于不支持可变泛型,您可以将参数包装在 @dataclass
.
旁注:
- 您想将基础
score
注释为返回int
而不是不在其中放置任何内容(默认为Any
)。 - 您想
raise
NotImplementedError
而不是return
它。
T = TypeVar("T")
class BaseScorecard(ABC, Generic[T]):
@abstractmethod
def score(self, args: T) -> int:
raise NotImplementedError
@dataclass
class Scorecard1Args:
var1: int
var2: int
var3: str
class Scorecard1(BaseScorecard[Scorecard1Args]):
def score(self, args: Scorecard1Args) -> int:
...
class Scorecard2(BaseScorecard[None]):
def score(self, _: None) -> int:
...
Scorecard1
然后有效地接受 var1
、var2
和 var3
,并且会像这样调用:
var1 = 1
var2 = 2
var3 = "abc"
args = Scorecard1Args(var1, var2, var3)
Scorecard1().score(args)
Scorecard2
实际上没有 args(也许它输出一个恒定的分数),但为了使其符合 BaseScoreboard
接口,我们让它采用 None
,它会像这样称呼:
Scorecard2().score(None)
如果您只想强制执行一致的命名,那么这可能有点矫枉过正。但是使用 Generic
的好处是,如果 X
相同,现在您可以认为 BaseScorecard[X]
的两个子类是等价的,而如果您在基本签名中使用 *args, **kwargs
,则会丢失类型安全。