Python的liskov替换原理和自定义init



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

  • 我如何更改我的代码,以便mypy了解函数签名是故意不同的?类似于CCD_ 1。我的目标是使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

类的初始化通常不包括在LSP中,LSP涉及实例的替换。根据LSP的定义:(重点矿井(

子类型要求:设ξ(x(是关于类型为T的对象x的可证明性质。那么,对于类型为S对象y,Γ(y(应该为真,其中S是T.的一个子类型

用Python的行话来说;类型为T"的对象x;是";T"的实例;。因此,类型T本身的操作不包含在LSP中。具体来说,这意味着子类型之间的实例化不需要是可替代的。

因此,__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构造是否真的有意义:这意味着调用者已经具有__init__0功能(对于await构造(,并且无论如何都必须为每个类使用自定义代码(以提供特定参数(。这意味着通常可以将整个async构造拉到调用者中。

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

也许您实际需要的是一个介于ImplA/ImplBAsyncInit之间的类,它知道如何在一个单独的方法中从ainit完成公共工作,然后这两个派生类可以从它们的ainit方法调用该方法。CCD_ 21和CCD_。这样一来,CCD_;被覆盖";方法,并且可以具有不同的签名。

例如:

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)中参数的顺序,使得类型与所调用的方法所期望的相匹配。

最新更新