与超类型不兼容的签名 - 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.

旁注:

  1. 您想将基础 score 注释为返回 int 而不是不在其中放置任何内容(默认为 Any)。
  2. 您想 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 然后有效地接受 var1var2var3,并且会像这样调用:

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,则会丢失类型安全。