如何使用 None 作为非可选变量的占位符而 mypy 不会抱怨它?

问题描述

直接上代码

from typing import Optional,List


class Bar:
    pass


class Foo:
    def __init__(self,bar_list: List[Optional[Bar]]) -> None:
        self.bar_list: List[Bar] = bar_list # mypy will signal an error here

        for i,bar in enumerate(self.bar_list):
            if bar is None:
                self.bar_list[i] = self.__default_bar()

    def __default_bar(self):
        # create default
        pass

mypy 这样做是完全合理的,但是我想说明的是,在使用这个类时,foo.barl_list 永远不应该有 None 元素,同时还提供了只分配一些元素的灵活性自定义值并让构造函数为其余值添加认值。

那么如何在不触发 mypy 错误的情况下保留此功能?一种方法是将 self.bar_list 初始化为一个空列表,然后附加所有值,但这会导致列表重新分配成本,在大列表的情况下可能会很大,因此我正在寻找替代方案。

解决方法

您尝试执行的操作与List[Bar]静态 类型根本不兼容。但是,您可以通过将列表转换为不同类型来告诉静态类型检查器您知道自己在做什么。您可以通过将每个 None 替换为实际的 Bar 实例来维护不变量。 (需要明确的是,类型检查器无法帮助您验证您确实这样做了。)

def __init__(self,bar_list: List[Optional[Bar]]) -> None:
    self.bar_list: List[Bar] = typing.cast(List[Bar],bar_list)

    for i,bar in enumerate(self.bar_list):
        if bar is None:
            self.bar_list[i] = self.__default_bar()
,

正如您所注意到的,MyPy 抱怨这个是完全正确的。当您在前一行中告诉它“小心,因为这可能不包含任何内容”时,您明确说“这将不包含任何内容”。您希望表达的是“这(最终)不会包含 None”,但这并不是真正的 MyPy 风格。如果您希望它干净利落地完成,则类型必须在创建时有效。只有这样,MyPy 才能帮助确保它保持有效。

我只是用列表理解来做这个。类似的东西

self.bar_list: List[Bar] = [
    b if b is not None else self.__default_bar() for b in bar_list
]

虽然这意味着要重新列出一份清单,但我还是建议您将其视为加分项而不是减分项。通常认为对函数的输入进行变异是非常糟糕的形式,除非这是该函数的唯一和明确的目的。