Python liskov替换原理和自定义init

Python liskov substition principle and custom init

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

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

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

也许您真正需要的是介于 ImplA/ImplBAsyncInit 之间的 class,它知道如何完成 ainit 的常见工作在一个单独的方法中,两个派生的 classes 然后可以从它们的 ainit 方法中调用。 ImplAImplB 都将从这个新的 class 而不是彼此派生。这样 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) 中参数的顺序从您的原始代码中翻转过来,以便类型与调用的方法所期望的相匹配。

从class初始化一般不在LSP中,它与实例的替换有关。根据 the definition of LSP:(强调我的)

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.

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

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


对于通过 classmethod 的替代构造函数来说,事情比较棘手:classmethod 可以在实例上调用,而 通常是 。因此,classmethod 被视为实例行为的一部分,因此受到类型检查器的子类型约束。
这尤其适用于备用 初始化程序 ,它们与常规方法没有区别。

目前没有正确的方法来制作初始化程序well-typed(例如通过对参数进行参数化),也没有具有相同可用性的替代设计(例如为该类型注册的外部构造函数)。
最简单的方法是实际告诉类型检查器一个方法不是子类型约束的一部分。对于 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 能力(await 构造) 必须使用每个 class 的自定义代码(以提供特定参数)。 这意味着通常可以将 整个 async 构造拉出到调用者中。