簡體   English   中英

強制抽象 class 屬性由具體 class 實現

[英]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"

這里的想法是,需要實現FooBaseFoo class 來指定foo屬性的值,但不需要覆蓋其他屬性( barbaz ),因為它們已經由提供的方法處理摘要 class。

從 MyPy 類型檢查的角度來看,是否可以強制Foo聲明屬性foo否則引發類型檢查錯誤?

編輯:

基本原理是FooBase是庫的一部分,應防止客戶端代碼在未指定foo值的情況下實現它。 然而,對於barbaz ,這些完全由圖書館管理,客戶不關心它們。

這是部分答案。 您可以使用

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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM