I would like to refactor the below to something type safe. What I have now gives a mypy "incompatible with supertype" error.
I understand this is due to the Liskov substitution principle:
- Contravariance of method arguments in the subtype.
- Covariance of return types in the subtype.
That is (if I'm understanding correctly), I can return A
or a subtype of A
, but I can only pass A
or a supertype of A
(neither of which would have a b
attribute) to B.add.
So, I "can't" do what I've been doing, and I'm looking for a way to refactor (more below code).
# 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)
The only thing that comes to mind is some parent type of A
and B
with no add
method.
class SuperAB:
# doesn't add
pass
class A(SuperAB):
# has add method
pass
class B(SuperAB):
# has add method
pass
That seems like a mess, but if it's the "Pythonic" thing to do, I'll go with it. I'm just wondering if there's another way (besides # type: ignore).
SOLUTION:
After playing "whack a mole" with various type errors, I got to this with the help of StackOverflow answers:
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")
)
Note that I can't do self.a + other.a
because I'll see a "T" has no attribute "a"
error. The above works, but it feels like the respondents here know more than I do, so I've taken their real advice and refactored.
One piece of advice I've seen enough to know is right is "B should have an A, not be an A." I'll confess that this one is beyond my understanding. B has and int (two actually, B.a and B.b). Those ints will do what ints do, but, in order to B.add(B)
, I've somehow got to put those ints into another B, and if I want that "somehow" to be polymorphic, I'm right back where I started. I'm obviously missing something fundamental about OOP.