重构:用实例参数重载方法的类型安全方法是什么,例如方法(自我,其他)?
Refactor: What is the type-safe way to overload methods with instance arguments, e.g. method(self, other)?
我想将以下内容重构为安全类型。我现在有一个 mypy "incompatible with supertype" 错误。
我理解这是由于 Liskov 替换原则:
- 子类型中方法参数的逆变。
- 子类型中 return 类型的协方差。
也就是说(如果我理解正确的话),我可以 return A
或 A
的 子类型 ,但是我只能将 A
或 A
的 超类型 传递给 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)
唯一想到的是 A
和 B
的某些父类型,没有 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 的基础知识。
我可能不会 B
从 A
继承。也就是说,对于 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 的 实例 并在必要时委托对它的调用。
我想将以下内容重构为安全类型。我现在有一个 mypy "incompatible with supertype" 错误。
我理解这是由于 Liskov 替换原则:
- 子类型中方法参数的逆变。
- 子类型中 return 类型的协方差。
也就是说(如果我理解正确的话),我可以 return A
或 A
的 子类型 ,但是我只能将 A
或 A
的 超类型 传递给 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)
唯一想到的是 A
和 B
的某些父类型,没有 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 的基础知识。
我可能不会 B
从 A
继承。也就是说,对于 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 的 实例 并在必要时委托对它的调用。