我有一個看起來像這樣的類結構:
class Base:
class Nested:
pass
def __init__(self):
self.nestedInstance = self.Nested()
其中Base每個子類都有自己的Nested類擴展原始類,如下所示:
class Sub(Base):
class Nested(Base.Nested):
pass
這完美地作業,并且實體的Sub屬性nestedInstance設定為實體Sub.Nested。
但是,在我的 IDE 中,nestedInstance屬性始終被視為 的實體Base.Nested,而不是繼承的Sub.Nested. 我怎樣才能使它nestedInstance被推斷為Sub.Nested而不是Base.Nested?(不必為每個子類添加額外的代碼;最好這一切都在Base.)
(順便說一句,我知道這是一個奇怪的結構,如有必要,我可以詳細說明我為什么選擇它,但我認為這對我的情況來說非常優雅,我希望有一個解決方案問題。)
uj5u.com熱心網友回復:
我不同意您試圖違反 Liskov 替換原則的說法。您只是在尋找一種方法讓靜態型別檢查器推斷nested_instance繼承自的類的型別Base是它們各自的Nested類。顯然,使用您擁有的代碼是不可能的;否則就沒有問題了。
實際上有一種方法可以最大限度地減少重復并完成您想要的。
泛型來拯救!
您可以將您的Base類定義為通用型別變數,其上限為Base.Nested. 當您定義Sub為 subclassBase時,您提供了對Sub.Nested作為具體型別引數的參考。這是設定:
from typing import Generic, TypeVar, cast
N = TypeVar("N", bound="Base.Nested")
class Base(Generic[N]):
nested_instance: N
class Nested:
pass
def __init__(self) -> None:
self.nested_instance = cast(N, self.Nested())
class Sub(Base["Sub.Nested"]):
class Nested(Base.Nested):
pass
這實際上就是您所需要的。有關泛型的更多資訊,我推薦PEP 484的相關部分。需要注意的幾點:
為什么我們需要bound?
如果我們只使用N = TypeVar("N"),如果我們想定義這樣的子類,型別檢查器將沒有問題:
class Broken(Base[int]):
class Nested(Base.Nested):
pass
但這將是一個問題,因為現在nested_instance屬性應該是 type int,這不是我們想要的。上界N將防止這導致mypy抱怨:
error: Type argument "int" of "Base" must be a subtype of "Nested" [type-var]
為什么要明確宣告nested_instance?
使類泛型的全部意義在于將一些型別變數(如N)系結到它,然后指示該類中的某些關聯型別實際上是N(甚至是多個)。我們基本上告訴型別檢查器期望nested_instance總是型別N,它必須提供,無論何時Base用于注釋某些東西。
然而,現在型別檢查器總是會抱怨,如果我們忽略了型別引數Base并嘗試了這樣的注釋x: Base:再次mypy告訴我們:
error: Missing type parameters for generic type "Base" [type-arg]
這可能是以這種方式使用泛型的唯一“缺點”。
為什么cast?
問題在于,在內部Base,nested_instance屬性被宣告為泛型型別N,而在內部Base.__init__,我們分配了特定型別的實體Base.Nested。盡管這看起來應該可行,但實際上并沒有。省略cast呼叫會導致以下mypy錯誤:
error: Incompatible types in assignment (expression has type "Nested", variable has type "N") [assignment]
引號是必要的嗎?
是的,匯入__future__.annotations在這里沒有幫助。我不完全確定為什么會這樣,但我相信在Base[...]使用的情況下,原因是它__class_getitem__實際上被呼叫了,你不能提供Sub.Nested給它,因為它甚至還沒有被定義。
完整的作業示例
from typing import Generic, TypeVar, cast
N = TypeVar("N", bound="Base.Nested")
class Base(Generic[N]):
nested_instance: N
class Nested:
pass
def __init__(self) -> None:
self.nested_instance = cast(N, self.Nested())
class Sub(Base["Sub.Nested"]):
class Nested(Base.Nested):
pass
def get_nested(obj: Base[N]) -> N:
return obj.nested_instance
def assert_instance_of_nested(nested_obj: N, cls: type[Base[N]]) -> None:
assert isinstance(nested_obj, cls.Nested)
if __name__ == '__main__':
sub = Sub()
nested = get_nested(sub)
assert_instance_of_nested(nested, Sub)
該腳本“按原樣”運行,并且mypy非常滿意。
這兩個函式僅用于演示目的,以便您了解如何利用泛型Base.
額外的健全性檢查
為了向您保證更多,您可以例如reveal_type(sub.nested_instance)在底部添加并mypy告訴您:
note: Revealed type is "[...].Sub.Nested"
這就是我們想要的。
如果我們宣告一個新的子類
class AnotherSub(Base["AnotherSub.Nested"]):
class Nested(Base.Nested):
pass
試試這個
a: AnotherSub.Nested
a = Sub().nested_instance
我們再次受到正確的譴責mypy:
error: Incompatible types in assignment (expression has type "[...].Sub.Nested", variable has type "[...].AnotherSub.Nested") [assignment]
希望這可以幫助。
附言
需要明確的是,您仍然可以在Base 不指定型別引數的情況下繼承自。這兩種方式都沒有運行時影響。只是一個嚴格的型別檢查器會抱怨它,因為它是通用的,就像你在list沒有指定型別引數的情況下注釋某些東西時它會抱怨一樣。(是的,list是通用的。)
此外,您的 IDE 是否真正正確地推斷出這一點當然取決于其內部型別檢查器與 Python 中的型別規則的一致性。例如,PyCharm 似乎可以按預期處理此設定。
uj5u.com熱心網友回復:
它被輸入為Base.Nested因為那是初始化范圍內的內容。如果你想宣告它nestedInstance實際上是別的東西,那么你可能需要實際型別提示它。
class Sub(Base):
class Nested(Base.Nested):
pass
nestedInstance: Nested
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/519739.html
