這個問題在這里已經有了答案: 重構:用實體引數多載方法的型別安全方法是什么,例如方法(self,other)? (2 個回答) 5 天前關閉。
得到了這個非常簡單的繼承案例。
我閱讀了一堆mypy檔案,但仍然無法弄清楚如何正確處理這些基本情況。
這對我來說是非常標準的 OOP 繼承,所以我無法想象mypy沒有一種干凈的方式來處理這些情況。
from __future__ import annotations
from dataclasses import dataclass
@dataclass
class Parent:
a: int = 0
def __add__(self, other: Parent) -> Parent:
a = self.a other.a
return self.__class__(a)
@dataclass
class Child(Parent):
b: int = 0
def __add__(self, other: Child) -> Child:
a = self.a other.a
b = self.b other.b
return self.__class__(a, b)
obj1 = Child(1)
obj2 = Child(1, 42)
print(obj1 obj2)
mypy錯誤資訊:
foo.py:18: error: Argument 1 of "__add__" is incompatible with supertype "Parent"; supertype defines the argument type as "Parent"
foo.py:18: note: This violates the Liskov substitution principle
foo.py:18: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
版本:
$ python --version
Python 3.10.4
$ mypy --version
mypy 0.971 (compiled: yes)
uj5u.com熱心網友回復:
從您的后續評論來看,我假設您實際上對__add__一種具有適當型別推斷mypy和其他型別檢查器的通用方法感興趣。仿制藥一如既往地拯救世界。
這是一個作業示例:
from __future__ import annotations
from dataclasses import dataclass, fields
from typing import TypeVar
T = TypeVar("T", bound="Parent")
@dataclass
class Parent:
a: int = 0
def __add__(self: T, other: T) -> T:
return self.__class__(**{
field.name: getattr(self, field.name) getattr(other, field.name)
for field in fields(self.__class__)
})
class Child(Parent):
b: int = 0
if __name__ == '__main__':
c1 = Child(a=1, b=2)
c2 = Child(a=2, b=3)
c3 = c1 c2
print(c3)
reveal_type(c3) # this line is for mypy
輸出當然Child(a=3, b=5)是mypy:
note: Revealed type is "[...].Child"
顯然,一旦任何子類Parent引入具有不可添加型別的欄位,這將中斷。
至于子型別問題,mypy實際上告訴你一切。你違反了 LSP。這不是 Python 特有的,mypy甚至也不是 Python 特有的。正如@Wombatz 所說,這就是型別(和子型別)必須如何作業才能被認為是合理的。
如果你有一個 type 的子型別T,它的介面必須是 的介面的超集T,而不是嚴格的子集。
附言
讓我嘗試用一??個稍微改變的例子稍微擴展一下 LSP 問題。
假設我有以下父類和子類:
from __future__ import annotations
from dataclasses import dataclass
@dataclass
class Parent:
n: float
def __add__(self, other: Parent) -> Parent:
return Parent(self.n other.n)
class Child(Parent):
def round_n(self) -> float:
return round(self.n, 0)
現在我撰寫了一個簡單的函式,它接受兩個 的實體Parent,將它們相加并列印結果:
def foo(obj1: Parent, obj2: Parent) -> None:
print(obj1 obj2)
到目前為止,一切都很好。這里沒有問題。
的簽名foo要求兩個引數都是 的實體Parent,這意味著它們也可以是的任何子類的實體Parent。這意味著它們也可以是Child.
假設foo是內部型別安全的,以下每個呼叫都是完全安全的:
parent = Parent(1.0)
child = Child(0.2)
foo(parent, parent)
foo(parent, child)
foo(child, parent)
foo(child, child)
輸出顯然是Parent(n=2.0)、Parent(n=1.2)、Parent(n=1.2)和Parent(n=0.4)。
但是,如果我現在決定重寫限制它只接受其他實體__add__的類的方法,會發生什么?假設我這樣寫:ChildChild
class Child(Parent):
def __add__(self, other: Child) -> Child:
return Child(self.round_n() other.round_n())
def round_n(self) -> float:
return round(self.n, 0)
暫時忽略繼承,這似乎完全合理且型別安全。我們注釋了with的other引數,它告訴型別檢查器只能將 的實體添加到 的實體中。這意味著我們可以安全地呼叫on 方法,因為所有實體都有該方法。__add__ChildChildChildround_notherChild
值得注意的是,Parent實體沒有該round_n方法。但這很好,因為我們以排除實體other的方式進行注釋。 Parent
但是現在我們的foo函式呢?
請記住,它允許兩個引數都是Parent實體,這也意味著Child實體。LSP 背后的整個想法在這里發揮作用。我們不想關心某些子類Parent可能具有的細節。我們假設無論子類做什么,都不會破壞Parent它繼承的介面。
具體來說,我們假設雖然子類可以覆寫該__add__方法,但這些更改不會限制我們呼叫它的方式。由于Parent.__add__可以用other的任何實體呼叫Parent,我們假設Child.__add__也可以用 的任何實體呼叫Parent。
不然怎么可能?可以有無限多的子類,Parent并且foo不可能期望驗證它們的每個介面仍然與 的介面兼容Parent。這就是作為子型別應該保證的。這就是里氏替換原則:
如果foo是正確的并且它接受型別Parent的引數,那么替換子型別的引數Child一定不會影響 的正確性foo。
我們已經確定這foo是正確的。但是,如果現在嘗試與以前相同的呼叫(使用更改的Child類),其中一個將失敗:
foo(child, parent)
它失敗了(對我們來說是可以預見的),并帶有以下回溯:
Traceback (most recent call last):
File "[...].py", line x, in <module>
foo(child, parent)
File "[...].py", line y, in foo
print(obj1 obj2)
File "[...].py", line z, in __add__
return Child(self.round_n() other.round_n())
AttributeError: 'Parent' object has no attribute 'round_n'
我們正在傳遞 a Parentas otherto Child.__add__,這是行不通的。
我希望這能更好地說明這個問題。
現在要做什么?
除了鏈接帖子中的建議外,最骯臟的解決方案如下:
class Child(Parent):
def __add__(self, other: Parent) -> Child:
if not isinstance(other, Child):
raise RuntimeError
return Child(self.round_n() other.round_n())
def round_n(self) -> float:
return round(self.n, 0)
這在技術上是正確的,只是對任何用戶都不是很好。用戶可能會期望他實際上可以使用a的Child.__add__方法,因此在實踐中,您可能會實作一些邏輯來回傳一些合理的東西,如下所示:otherParent
class Child(Parent):
def __add__(self, other: Parent) -> Child:
if not isinstance(other, Child):
return Child(self.round_n() other.n)
return Child(self.round_n() other.round_n())
def round_n(self) -> float:
return round(self.n, 0)
順便注意一下,將回傳型別限制為子型別是沒有問題的。由于這篇文章太冗長了,我將把它作為練習留給讀者來推斷為什么會這樣。
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/519732.html
