重写方法签名时如何避免违反里氏替换原则

HuL*_*iCa 2 python abstract-class liskov-substitution-principle python-3.x mypy

我有一个DataWriter定义抽象方法的抽象类write()。该类应该是一组动态具体类的基类,其中每个类都旨在实现其自己的方法版本write()。为了定义meta方法参数的数据类型,我创建了WriterMetawrite()类型,如下所示:

WriterMeta = typing.Union[GSheetWritable, S3Writable, LocalWritable]

每个具体类将负责处理符合联合的不同类型之一,但 linter mypy似乎没有掌握这一点,因为当我write()使用其中一种类型定义具体类的方法的签名时参数的联合meta,它标记了 a Liskov substituion principle violation,我认为它不存在,因为具体类是抽象类的子集,这意味着父类可以毫无问题地替换子类。

这是我的代码:

class LocalWritable(typing.TypedDict):
    file_name: str


class GSheetWritable(typing.TypedDict):
    tab_name: str


class S3Writable(typing.TypedDict):
    data_name: str
    table_name: str


WriterMeta = typing.Union[GSheetWritable, S3Writable, LocalWritable]

class GSheetOutputWriter(DataWriter):
    def __init__(
        self, google_driver: GoogleApiDriver, folder: str, settings, timestamp, env
    ):
        self._connector = google_driver
        self.folder = folder
        self.settings = settings
        self.timestamp = timestamp
        self.env = env
        self.file_name = self.get_file_name()
        self._target = self.create_gsheet()
        self.new = True

    def get_file_name(self) -> str:
        file_name = (
            "boxes_shipping_costs_"
            + self.settings["volume_source"]
            + "_"
            + (
                self.timestamp
                if self.settings["volume_source"] == "adhoc"
                else self.settings["scm_week"]
            )
        )

        return file_name

    def create_gsheet(self):
        gsheet = self.connector.sheet_create(self.file_name, folder_id=self.folder)
        gsheet.worksheet("Sheet1").resize(rows=1, cols=1)

        return gsheet

    @property
    def connector(self) -> typing.Any:
        return self._connector

    @property
    def target(self) -> typing.Any:
        return self._target

    def write(self, data: pd.DataFrame, meta: GSheetWritable, versionize: bool):
        data = data.replace({np.nan: 0, np.Inf: "Inf"})

        print("Writing '{}' table to gsheet.".format(meta["tab_name"]))
        if self.new:
            tab = self.connector.get_worksheet(self.target.url, "Sheet1")
            self.connector.rename_worksheet(tab, meta["tab_name"])
            self.new = False
        else:
            tab = self.connector.add_worksheet(
                self.target, meta["tab_name"], rows=1, cols=1
            )

        time.sleep(random.randint(30, 60))
        self.connector.update_worksheet(
            tab, [data.columns.values.tolist()] + data.values.tolist()
        )
Run Code Online (Sandbox Code Playgroud)

我对里氏替换原理的理解对吗?我如何重构这组类以使mypy接受它们?

STe*_*kov 5

为什么会失败

\n

当您声明对某种类型TUnion在您的情况下)进行操作的抽象类方法时,从 mypy 的角度来看,这意味着以下内容:每个子类都必须实现接受类型T或更宽类型的方法。方法,仅接受 的一部分T,违反了 LSP:每当您引用抽象类时,您都认为所有类型都T可以在该方法中使用,而具体实现不允许这样做。

\n

输入安全方式 \xe2\x80\x93 通用解决方案

\n

您可以为此使用泛型类。

\n
import pandas as pd\nfrom abc import ABC, abstractmethod\nfrom typing import Generic, TypeVar, TypedDict\n\nclass LocalWritable(TypedDict):\n    file_name: str\n\nclass GSheetWritable(TypedDict):\n    tab_name: str\n\nclass S3Writable(TypedDict):\n    data_name: str\n    table_name: str\n\n# This variable is compatible with any of classes or their subclasses,\n# not with `Union` of them as opposed to TypeVar(bound=Union[...]) usage.\n_OutputT = TypeVar(\'_OutputT\', GSheetWritable, S3Writable, LocalWritable)\n\nclass DataWriter(ABC, Generic[_OutputT]):\n    @abstractmethod\n    def write(self, data: pd.DataFrame, meta: _OutputT, versionize: bool) -> None: ...\n\nclass GSheetDataWriter(DataWriter[GSheetWritable]):\n    def write(self, data: pd.DataFrame, meta: GSheetWritable, versionize: bool) -> None:\n        pass\n        # Implementation goes here\n
Run Code Online (Sandbox Code Playgroud)\n