迫使混凝土类实现的抽象类属性

发布于 2025-02-08 20:55:02 字数 696 浏览 3 评论 0 原文

考虑此抽象类和实现它的类:

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 才能指定 foo 属性,但是其他属性( bar baz )不需要被覆盖,因为它们已经通过抽象类提供的方法处理。

从MyPy类型检查的角度来看,是否可以强制强制 foo 声明属性 foo 并引起类型检查错误否则?

编辑:

基本原理是 foobase 是库的一部分,应防止客户代码实现它,而无需指定 foo 的值。对于 bar baz ,这些完全由库管理,客户不在乎它们。

Considering this abstract class and a class implementing it:

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"

The idea here is that a Foo class that implements FooBase would be required to specify the value of the foo attribute, but the other attributes (bar and baz) would not need to be overwritten, as they're already handle by a method provided by the abstract class.

From a MyPy type-checking perspective, is it possible to force Foo to declare the attribute foo and raise a type-checking error otherwise?

EDIT:

The rationale is that FooBase is part of a library, and the client code should be prevented from implementing it without specifying a value for foo. For bar and baz however, these are entirely managed by the library and the client doesn't care about them.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

我只土不豪 2025-02-15 20:55:02

以前,此类问题的“解决方案”之一是堆叠 @property @classmethod @abstractmethod 一起产生“ 类属性。

摘要 @classmethod 或 @staticmethod @property 的表现真的很差,因此已经决定这样的链式装饰器是 reteprecated开始python 3.11 ,现在将使用 mypy 的工具错误

。如,特别是如果您需要属性来进行一些昂贵/延迟的访问。诀窍是使用 @abstractmethod 与子类 typing.protocol 进行补充。

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 的子类不同,如果您尝试使用抽象属性实例化类,则会导致运行时错误。

此外,上述方法不支持 foo = distibcriptor()的使用,类似于用 @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()

两个类通过类型检查,并且按预期的是在运行时实际工作,尽管在运行时又没有执行任何执行。

Previously, one the "solutions" to this type of problem was to stack @property, @classmethod, and @abstractmethod together to produce an "abstract class property`.

According to CPython issue #89519, chaining descriptor decorators like @classmethod or @staticmethod with @property can behave really poorly, so it has been decided that chaining decorators like this is deprecated beginning Python 3.11, and will now error with tools like mypy.

There is an alternative solution if you really need something that behaves like an abstract class property, as explained in this comment, especially if you need a property for some expensive/delayed accessing. The trick is to supplement using @abstractmethod with subclassing typing.Protocol.

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"

Note that although linters can catch this type of error, it is not enforced at runtime, unlike creating a subclass of abc.ABC which causes a runtime error if you try to instantiate a class with an abstract property.

Additionally, the above approach does not support the use of foo = Descriptor(), similar to implementing an attribute with a @property instead. To cover both cases, you'll need to use the following:

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()

Both classes pass type checks and actually work at runtime as intended, although again nothing is enforced at runtime.

情话已封尘 2025-02-15 20:55:02

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

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 的具体类来减轻这种情况的。以下将在课程本身上引起错误。

@final
class Foo(FooBase):  # error: Final class __main__.Foo has abstract attributes "foo"
   ...

This is a partial answer. You can use

class FooBase(ABC):
    @property
    @classmethod
    @abstractmethod
    def foo(cls) -> str:
        ...

class Foo(FooBase):
    foo = "hi"

def go(f: FooBase) -> str:
    return f.foo

It's only partial because you'll only get a mypy error if you try to instantiate Foo without an initialized foo, like

class Foo(FooBase):
    ...

Foo()  # error: Cannot instantiate abstract class "Foo" with abstract attribute "foo"

This is the same behaviour as when you have a simple @abstractmethod. Only when instantiating it is the error raised. This is expected because Foo might not be intended as a concrete class, and may itself be subclassed. You can mitigate this somewhat by stating it is a concrete class with typing.final. The following will raise an error on the class itself.

@final
class Foo(FooBase):  # error: Final class __main__.Foo has abstract attributes "foo"
   ...
删除会话 2025-02-15 20:55:02

您可以使用 init_subclass __ ,而不是依靠用户在班级主体内部设置属性-0487/“ rel =” nofollow noreferrer“> pep 487 。此外,您应该将 typing.classvar 用于类变量,否则将与实例变量混合。

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 Database(DB, user=..., password=...):
    pass

这些变量可以设置来创建 database.connection.connection 类变量。

这种方法的一个缺点是,进一步的子类将需要继续提供类参数,通常可以通过实现自己的 __ 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上产生错误(数据库,reconnect = true):

Rather than relying on the user to set an attribute inside of the class body, you can instead mandate a value using __init_subclass__ as proposed by PEP 487. Additionally, you should use typing.ClassVar for class variables, otherwise it'll mix with instance variables.

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

This syntax is clean for the user, especially when you need something more complex than setting a class variable like

class Database(DB, user=..., password=...):
    pass

which could get setup to create a Database.connection class variable.

The one downside with this approach is that further subclasses will need to continue supplying the class parameters, which can usually be fixed by implementing your own __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

Good linters should be able to recognize this type of pattern, producing errors on class Foo(FooBase): without producing errors on class SubDatabse(Database, reconnect=True):.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文