Python liskov替换原理及自定义init
我正在编写带有提供异步初始化的自定义初始化函数的类。这一切都运行良好,除了当我创建子类并覆盖 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 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
LSP 通常不涵盖类的初始化,LSP 涉及实例的替换。根据 LSP 的定义:(强调我的)
在 Python 的行话中,“T 类型的对象 x”是“T 的实例”。因此,LSP 不涵盖类型 T 本身的操作。具体来说,这意味着子类型之间的实例化不需要是可替换的。
因此,
__new__
和__init__
通常都被类型检查器免除子类型约束,因为它们的规范用法是在实例化期间。对于通过
classmethod
替代构造函数来说,事情会更加棘手:可以在实例上调用classmethod
,并且通常。因此,classmethod
被视为实例行为的一部分,因此受到类型检查器的子类型约束。这尤其适用于与常规方法无法区分的备用初始化器。
目前没有适当的方法来使初始化器类型化(例如通过对参数进行参数化),也没有具有同等可用性的替代设计(例如为该类型注册的外部构造函数)。
最简单的方法是实际告诉类型检查器该方法不是子类型约束的一部分。对于 MyPy,这是通过
# type:ignore [override]
完成的。但是,通常值得考虑的是,跨子类型不可比较的替代
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)
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
: aclassmethod
can be – and commonly is – called on an instance. As such, aclassmethod
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]
.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 hasasync
capability (toawait
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.错误消息是正确的 - 您实现的类要求调用者确切地知道他们正在操作的对象的类型(
ImplA
与ImplB
),以便能够调用ainit
,但是通过让一个从另一个派生,你暗示(或者,实际上,声明)他们不需要知道。也许您真正需要的是
ImplA
/ImplB
和AsyncInit
之间的一个类,它知道如何完成ainit< 中的常见工作/code> 在一个单独的方法中,然后两个派生类可以从它们的
ainit
方法中调用该方法。然后,ImplA
和ImplB
都将从这个新类派生,而不是彼此派生。这样,ainit
就不是“重写”方法,并且可以具有不同的签名。例如:
我应该注意到,我已经从原始代码中翻转了
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
versusImplB
) in order to be able to callainit
, 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
andAsyncInit
that knows how to do the common work fromainit
in a separate method that the two derived classes can then call from theirainit
methods. BothImplA
andImplB
would then derive from this new class instead of each other. That wayainit
is not an "overridden" method and can have a different signature.For example:
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.