重构:用实例参数重载方法的类型安全方法是什么,例如方法(自我,其他)?

Refactor: What is the type-safe way to overload methods with instance arguments, e.g. method(self, other)?

我想将以下内容重构为安全类型。我现在有一个 mypy "incompatible with supertype" 错误。

我理解这是由于 Liskov 替换原则:

也就是说(如果我理解正确的话),我可以 return AA 子类型 ,但是我只能将 AA 超类型 传递给 B.add.

所以,我 "can't" 做我一直在做的事情,我正在寻找一种重构的方法(更多代码见下文)。

# python 3.7

class A:
    def __init__(self, a: int) -> None:
        self.a = a

    def add(self, other: "A") -> "A":
        return type(self)(a=self.a + other.a)


class B(A):
    def __init__(self, a: int, b: int) -> None:
        super().__init__(a=a)
        self.b = b

    def add(self, other: "B") -> "B": # Argument 1 of "add" incompatible with supertype "A"
        return type(self)(a=self.a + other.a, b=self.b + other.b)

唯一想到的是 AB 的某些父类型,没有 add 方法。

class SuperAB:
    # doesn't add
    pass

class A(SuperAB):
    # has add method
    pass

class B(SuperAB):
    # has add method
    pass

这似乎是一团糟,但如果这是 "Pythonic" 要做的事,我会同意的。我只是想知道是否还有其他方法(除了 # type: ignore)。

解决方案:

在玩了 "whack a mole" 各种类型错误之后,我在 Whosebug 答案的帮助下得到了这个:

T = TypeVar("T")

class A(Generic[T]):
    def __init__(self, a: int) -> None:
        self.a = a

    def add(self, other: T) -> "A":
        return type(self)(a=self.a + getattr(other, "a"))


class B(A["B"]):
    def __init__(self, a: int, b: int) -> None:
        super().__init__(a=a)
        self.b = b

    def add(self, other: T) -> "B":
        return type(self)(
            a=self.a + getattr(other, "a"), b=self.b + getattr(other, "b")
        )

请注意,我不能执行 self.a + other.a,因为我会看到 "T" has no attribute "a" 错误。上面的方法可行,但感觉这里的受访者比我知道的更多,所以我接受了他们的真实建议并进行了重构。

一条我看得够多的建议是正确的 "B should have an A, not be an A." 我承认这条建议超出了我的理解范围。 B 和int(实际上是两个,B.a和B.b)。这些整数将做整数所做的事情,但是,为了 B.add(B),我必须以某种方式将这些整数放入另一个 B,如果我希望 "somehow" 是多态的,我是对的回到我开始的地方。我显然遗漏了一些关于 OOP 的基础知识。

我可能不会 BA 继承。也就是说,对于 B 应该继承自 A 的情况,您可能正在寻找您在 Java 的 Comparable 中看到的那种模式,其中class 接受一个 subclass 作为类型参数:

from abc import ABCMeta, abstractmethod
from typing import Generic, TypeVar

T = TypeVar('T')

class A(Generic[T], metaclass=ABCMeta):
    @abstractmethod
    def add(self, other: T) -> T:
        ...

class B(A['B']):
    def add(self, other: 'B') -> 'B':
        ...

请注意,我已将 A 标记为抽象,并将 add 标记为抽象方法。在这种情况下,add 的根声明是一个具体的方法,或者一个 class 有一个具体的 add 到 sub[=26 没有多大意义=] 另一个 class 与具体 add.

如果您想保留现有的 class 层次结构,您可能应该修改 B 的 add 方法,以便它在接受 A 的某个实例时表现得合理。例如,可以执行以下操作:

from typing import overload, Union

class A:
    def __init__(self, a: int) -> None:
        self.a = a

    def add(self, other: A) -> A:
        return A(self.a + other.a)


class B(A):
    def __init__(self, a: int, b: int) -> None:
        super().__init__(a=a)
        self.b = b

    # Use overloads so we can return a more precise type when the
    # argument is B instead of `Union[A, B]`.
    @overload
    def add(self, other: B) -> B: ...
    @overload
    def add(self, other: A) -> A: ...

    def add(self, other: A) -> Union[A, B]:
        if isinstance(other, B):
            return B(self.a + other.a, self.b + other.b)
        else:
            return A(self.a + other.a)

b1 = B(1, 2)
b2 = B(3, 4)
a = A(5)

reveal_type(b1.add(b2))  # Revealed type is B
reveal_type(b1.add(a))   # Revealed type is A

# You get this for free, even without the overloads/before making
# any of the changes I proposed above.
reveal_type(a.add(b1))   # Revealed type is A

这将恢复 Liskov 等类型检查。它还使您的 add 方法对称,从可用性的角度来看,这可能是正确的做法。

如果您想要这种行为(例如,如果 a.add(b1)b1.add(a) 不受欢迎),您可能需要重组您的代码并使用 user2357112 之前建议的方法。与其让 B 继承 A,不如让它包含 A 的 实例 并在必要时委托对它的调用。