Python liskov替换原理及自定义init

发布于 2025-01-16 05:39:05 字数 1000 浏览 2 评论 0原文

我正在编写带有提供异步初始化的自定义初始化函数的类。这一切都运行良好,除了当我创建子类并覆盖 async init 函数时,mypy 告诉我我违反了 liskov 替换原则。这给我留下了两个问题:

  • 如何更改我的代码,以便 mypy 理解函数签名是故意不同的?与__init__类似。我的目标是使 ImplA 和 ImplB 尽可能简单,因为这些是我的用户实现的类。 AsyncInit 可能非常复杂。
  • __init__ 是否违反了里氏替换原则?
from typing import TypeVar, Type, Any

TChild = TypeVar("TChild", bound="AsyncInit")


class AsyncInit:
    @classmethod
    async def new(cls: Type[TChild], *args: Any, **kwargs: Any) -> TChild:
        self = super().__new__(cls)
        await self.ainit(*args, **kwargs)  # type: ignore # ignore that TChild does not have `ainit` for now
        return self


class ImplA(AsyncInit):
    async def ainit(self, arg1: int, arg2: float) -> None:
        self.a = arg1
        self.b = arg2


class ImplB(ImplA):
    async def ainit(self, arg1: str, arg2: float, arg3: int) -> None:
        await super().ainit(arg2, arg3)
        self.c = arg1

I am writing classes with custom init functions that provide async initialization. This all works well, except that when I create a subclass and override the async init function, mypy tells me I am violating the liskov substitution principle. This leaves me with two questions:

  • How can I change my code so mypy understands that the function signature is different on purpose? Similarly to __init__. My goal is to make ImplA and ImplB as simple as possible as these are classes implemented by my users. AsyncInit can be super complicated.
  • Does __init__ violate the liskov substitution principle?
from typing import TypeVar, Type, Any

TChild = TypeVar("TChild", bound="AsyncInit")


class AsyncInit:
    @classmethod
    async def new(cls: Type[TChild], *args: Any, **kwargs: Any) -> TChild:
        self = super().__new__(cls)
        await self.ainit(*args, **kwargs)  # type: ignore # ignore that TChild does not have `ainit` for now
        return self


class ImplA(AsyncInit):
    async def ainit(self, arg1: int, arg2: float) -> None:
        self.a = arg1
        self.b = arg2


class ImplB(ImplA):
    async def ainit(self, arg1: str, arg2: float, arg3: int) -> None:
        await super().ainit(arg2, arg3)
        self.c = arg1

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

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

发布评论

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

评论(2

酷到爆炸 2025-01-23 05:39:05

LSP 通常不涵盖类的初始化,LSP 涉及实例的替换。根据 LSP 的定义:(强调我的)

子类型要求:设 phi(x) 是关于类型 T 的对象 x 的可证明属性。那么 phi(y) 对于 S 类型的对象 y 应该为真,其中 S 是 T 的子类型。

在 Python 的行话中,“T 类型的对象 x”是“T 的实例”。因此,LSP 不涵盖类型 T 本身的操作。具体来说,这意味着子类型之间的实例化不需要是可替换的。

因此,__new____init__ 通常都被类型检查器免除子类型约束,因为它们的规范用法是在实例化期间。


对于通过 classmethod 替代构造函数来说,事情会更加棘手:可以在实例上调用 classmethod,并且通常。因此,classmethod 被视为实例行为的一部分,因此受到类型检查器的子类型约束。
这尤其适用于与常规方法无法区分的备用初始化器。

目前没有适当的方法来使初始化器类型化(例如通过对参数进行参数化),也没有具有同等可用性的替代设计(例如为该类型注册的外部构造函数)。
最简单的方法是实际告诉类型检查器该方法不是子类型约束的一部分。对于 MyPy,这是通过 # type:ignore [override] 完成的。

class ImplB(ImplA):
    async def ainit(self, arg1: str, arg2: float, arg3: int) -> None:  # type: ignore [override]
        await super().ainit(arg3, arg2)
        self.c = arg1

但是,通常值得考虑的是,跨子类型不可比较的替代 async 构造是否确实有意义:这意味着调用者已经具有 async > 能力(等待构造)并且无论如何都必须使用每个类的自定义代码(以提供特定参数)。
这意味着通常可以将整个async结构拉到调用者中。

The initialization from a class is generally not covered by LSP, which is concerned with the substitution of instances. As per the definition of LSP: (emphasis mine)

Subtype Requirement: Let ϕ(x) be a property provable about objects x of type T. Then ϕ(y) should be true for objects y of type S where S is a subtype of T.

In Python's lingo, "objects x of type T" are "instances of T". As such, operations on type T itself are not covered by LSP. In specific, that means instantiation between subtypes does not need to be substitutable.

As such, both __new__ and __init__ are commonly exempt by type checkers from subtyping constraints as their canonical usage is during instantiation.


Things are trickier for alternate constructors via classmethod: a classmethod can be – and commonly is – called on an instance. As such, a classmethod is considered part of the instance behaviour and thus subject to subtyping constraints by type checkers.
This especially applies to alternate initializers which are not distinguishable from regular methods.

There is currently no proper way to make the initializers well-typed (e.g. by parameterizing over parameters) nor alternative designs with equal usability (e.g. external constructors registered for the type).
The simplest means is to actually tell the type checker that a method is not part of subtyping constraints. For MyPy, this is done via # type: ignore [override].

class ImplB(ImplA):
    async def ainit(self, arg1: str, arg2: float, arg3: int) -> None:  # type: ignore [override]
        await super().ainit(arg3, arg2)
        self.c = arg1

However, it is usually worth to consider whether an alternate async construction that is not comparable across subtypes actually makes sense: it means the caller already has async capability (to await the construction) and has to use custom code per class (to provide specific arguments) anyway.
This means it is usually possible to pull the entire async construction out into the caller.

沫离伤花 2025-01-23 05:39:05

错误消息是正确的 - 您实现的类要求调用者确切地知道他们正在操作的对象的类型(ImplAImplB),以便能够调用ainit,但是通过让一个从另一个派生,你暗示(或者,实际上,声明)他们不需要知道。

也许您真正需要的是 ImplA/ImplBAsyncInit 之间的一个类,它知道如何完成 ainit< 中的常见工作/code> 在一个单独的方法中,然后两个派生类可以从它们的 ainit 方法中调用该方法。然后,ImplAImplB 都将从这个新类派生,而不是彼此派生。这样,ainit 就不是“重写”方法,并且可以具有不同的签名。

例如:

from typing import TypeVar, Type, Any

TChild = TypeVar("TChild", bound="AsyncInit")


class AsyncInit:
    @classmethod
    async def new(cls: Type[TChild], *args: Any, **kwargs: Any) -> TChild:
        self = super().__new__(cls)
        await self.ainit(*args, **kwargs)  # type: ignore # ignore that TChild does not have `ainit` for now
        return self


class NewBaseClass(AsyncInit):
    async def _ainit(self, arg1: int, arg2: float) -> None:
        self.a = arg1
        self.b = arg2


class ImplA(NewBaseClass):
    async def ainit(self, arg1: int, arg2: float) -> None:
        await super()._ainit(arg1, arg2)


class ImplB(NewBaseClass):
    async def ainit(self, arg1: str, arg2: float, arg3: int) -> None:
        await super()._ainit(arg3, arg2)
        self.c = arg1

我应该注意到,我已经从原始代码中翻转了 await super().ainit(arg2, arg3) 中参数的顺序,以便类型与被调用方法期望的类型匹配。

The error message is correct - your classes as implemented require the caller to know exactly the type of the object they're operating on (ImplA versus ImplB) in order to be able to call ainit, but by having one derived from the other you're implying (or, indeed, stating) that they shouldn't need to know.

Perhaps what you actually need is a class in between ImplA/ImplB and AsyncInit that knows how to do the common work from ainit in a separate method that the two derived classes can then call from their ainit methods. Both ImplA and ImplB would then derive from this new class instead of each other. That way ainit is not an "overridden" method and can have a different signature.

For example:

from typing import TypeVar, Type, Any

TChild = TypeVar("TChild", bound="AsyncInit")


class AsyncInit:
    @classmethod
    async def new(cls: Type[TChild], *args: Any, **kwargs: Any) -> TChild:
        self = super().__new__(cls)
        await self.ainit(*args, **kwargs)  # type: ignore # ignore that TChild does not have `ainit` for now
        return self


class NewBaseClass(AsyncInit):
    async def _ainit(self, arg1: int, arg2: float) -> None:
        self.a = arg1
        self.b = arg2


class ImplA(NewBaseClass):
    async def ainit(self, arg1: int, arg2: float) -> None:
        await super()._ainit(arg1, arg2)


class ImplB(NewBaseClass):
    async def ainit(self, arg1: str, arg2: float, arg3: int) -> None:
        await super()._ainit(arg3, arg2)
        self.c = arg1

I should note that I've flipped the order of the arguments in await super().ainit(arg2, arg3) from your original code such that the types match what the called method expects.

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