[英]Force an abstract class attribute to be implemented by concrete class
考慮這個抽象 class 和一個 class 實現它:
from abc import ABC
class FooBase(ABC):
foo: str
bar: str
baz: int
def __init__(self):
self.bar = "bar"
self.baz = "baz"
class Foo(FooBase):
foo: str = "hello"
這里的想法是,需要實現FooBase
的Foo
class 來指定foo
屬性的值,但不需要覆蓋其他屬性( bar
和baz
),因為它們已經由提供的方法處理摘要 class。
從 MyPy 類型檢查的角度來看,是否可以強制Foo
聲明屬性foo
否則引發類型檢查錯誤?
編輯:
基本原理是FooBase
是庫的一部分,應防止客戶端代碼在未指定foo
值的情況下實現它。 然而,對於bar
和baz
,這些完全由圖書館管理,客戶不關心它們。
這是部分答案。 您可以使用
class FooBase(ABC):
@property
@classmethod
@abstractmethod
def foo(cls) -> str:
...
class Foo(FooBase):
foo = "hi"
def go(f: FooBase) -> str:
return f.foo
這只是部分原因,因為如果您嘗試在沒有初始化foo
的情況下實例化Foo
,您只會收到 mypy 錯誤,例如
class Foo(FooBase):
...
Foo() # error: Cannot instantiate abstract class "Foo" with abstract attribute "foo"
這與您有一個簡單的@abstractmethod
時的行為相同。 只有在實例化它時才會引發錯誤。 這是意料之中的,因為Foo
可能不打算作為一個具體的類,並且它本身可能是子類。 您可以通過使用typing.final
聲明它是一個具體的類來緩解這種情況。 以下將在類本身上引發錯誤。
@final
class Foo(FooBase): # error: Final class __main__.Foo has abstract attributes "foo"
...
與其依賴用戶在 class 主體內部設置屬性,不如按照PEP 487的建議使用__init_subclass__
指定一個值。 此外,您應該將typing.ClassVar
用於 class 變量,否則它會與實例變量混合。
from typing import Any, ClassVar
class FooBase:
foo: ClassVar[str]
def __init_subclass__(cls, /, *, foo: str, **kwargs: Any) -> None:
cls.foo = foo
return super().__init_subclass__(**kwargs)
class Foo(FooBase, foo="hello"):
pass
這種語法對用戶來說是干凈的,特別是當你需要比設置 class 變量更復雜的東西時
class Database(DB, user=..., password=...):
pass
可以設置它來創建Database.connection
class 變量。
這種方法的一個缺點是更多的子類將需要繼續提供 class 參數,這通常可以通過實現您自己的__init_subclass__
來解決:
class MainDatabase(DB, user=..., password=...):
def __init_subclass__(cls, /, reconnect: bool = False, **kwargs: Any) -> None:
# Create a new connection.
if reconnect:
kwargs.setdefault("user", cls.user)
kwargs.setdefault("password", cls.password)
return super().__init_subclass__(**kwargs)
# Re-use the current connection.
else:
return super(DB, cls).__init_subclass__(**kwargs)
class SubDatabase(Database, reconnect=True, user=..., password=...):
pass
好的 linters 應該能夠識別這種類型的模式,在class Foo(FooBase):
上產生錯誤,而不會在class SubDatabse(Database, reconnect=True):
上產生錯誤。
以前,此類問題的“解決方案”之一是將@property、 @property
@classmethod
和@abstractmethod
在一起以生成“抽象 class 屬性”。
根據CPython 問題 #89519 ,將@classmethod
或@staticmethod
等描述符裝飾器與@property
鏈接起來可能表現得很差,因此已決定從 Python 3.11 開始棄用像這樣的鏈接裝飾器,現在使用mypy
等工具會出錯。
如果您真的需要一些行為類似於抽象 class 屬性的東西,那么還有一個替代解決方案,如本評論中所述,特別是如果您需要一個屬性來進行一些昂貴/延遲的訪問。 訣竅是使用帶有子類typing.Protocol
的@abstractmethod
進行補充。
from typing import ClassVar, Protocol
class FooBase(Protocol):
foo: ClassVar[str]
class Foo(FooBase):
foo = "hello"
class Bar(FooBase):
pass
Foo()
Bar() # Cannot instantiate abstract class "Bar" with abstract attribute "foo"
請注意,盡管 linters 可以捕獲此類錯誤,但它不會在運行時強制執行,這與創建 abc.ABC 的子類不同,如果您嘗試使用抽象屬性實例化abc.ABC
,則會導致運行時錯誤。
此外,上述方法不支持使用foo = Descriptor()
,類似於使用@property
實現屬性。 要涵蓋這兩種情況,您需要使用以下內容:
from typing import Any, ClassVar, Optional, Protocol, Type, TypeVar, Union
T_co = TypeVar("T_co", covariant=True)
class Attribute(Protocol[T]):
def __get__(self, instance, owner=None) -> T_co:
...
class FooBase(Protocol):
foo: ClassVar[Union[Attribute[str], str]]
class Foo(FooBase):
foo = "hello"
class Foo:
def __get__(self, instance: Any, owner: Optional[Type] = None) -> str:
return "hello"
class Bar(FooBase):
foo = Foo()
Foo()
Bar()
這兩個類都通過了類型檢查並且實際上在運行時按預期工作,盡管在運行時也沒有強制執行任何操作。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.