mypy错误,Union / Optional的重载,“重载的函数签名1和2与不兼容的返回类型重叠”

时间:2018-11-28 23:32:17

标签: python python-3.x overload-resolution mypy union-types

所以,让我们从一个例子开始。假设我们有几种可以组合在一起的类型,假设我们使用__add__来实现这一点。不幸的是,由于无法控制的情况,所有内容都必须是“空的”,因此我们被迫在所有地方使用Optional

from typing import Optional, List, overload
class Foo:
    value: int
    def __init__(self, value: int) -> None:
        self.value = value
    def __add__(self, other: 'Foo') -> 'Optional[Foo]':
        result = self.value - other.value
        if result > 42:
            return None
        else:
            return Foo(result)

class Bar:
    value: str
    def __init__(self, value: str) -> None:
        self.value = value
    def __add__(self, other: 'Bar') -> 'Optional[Bar]':
        if len(self.value) + len(other.value) > 42:
            return None
        else:
            return Bar(self.value + other.value)

class Baz:
    value: List[str]
    def __init__(self, value:List[str]) -> None:
        self.value = value
    def __add__(self, other: 'Bar') -> 'Optional[Baz]':
        if len(self.value) + 1 > 42:
            return None
        else:
            return Baz([*self.value, other.value])


@overload
def Add(this: Optional[Foo], that: Optional[Foo]) -> Optional[Foo]:
    ...
@overload
def Add(this: Optional[Bar], that: Optional[Bar]) -> Optional[Bar]:
    ...
@overload
def Add(this: Optional[Baz], that: Optional[Bar]) -> Optional[Baz]:
    ...
def Add(this, that):
    if this is None or that is None:
        return None
    else:
        return this + that

我们需要实用程序函数,该函数可以为我们做空检查,但通常可以处理“可组合”类型。大多数类型只能与它们自己组合,并且为了更符合我的实际用例,可以说一种类型与另一种类型组合。我希望overload装饰器可以在这里有所帮助,但是mypy抱怨:

mcve4.py:35: error: Overloaded function signatures 1 and 2 overlap with incompatible return types
mcve4.py:35: error: Overloaded function signatures 1 and 3 overlap with incompatible return types
mcve4.py:38: error: Overloaded function signatures 2 and 3 overlap with incompatible return types

使用mypy版本:mypy 0.641

请注意,如果我消除Optional的疯狂,mypy不会抱怨。我什至可以保留其中之一作为可选项!:

from typing import List, overload
class Foo:
    value: int
    def __init__(self, value: int) -> None:
        self.value = value
    def __add__(self, other: 'Foo') -> 'Foo':
        result = self.value - other.value
        return Foo(result)

class Bar:
    value: str
    def __init__(self, value: str) -> None:
        self.value = value
    def __add__(self, other: 'Bar') -> 'Bar':
        return Bar(self.value + other.value)

class Baz:
    value: List[str]
    def __init__(self, value:List[str]) -> None:
        self.value = value
    def __add__(self, other: 'Bar') -> 'Optional[Baz]':
        return Baz([*self.value, other.value])


@overload
def Add(this: Foo, that: Foo) -> Foo:
    ...
@overload
def Add(this: Bar, that: Bar) -> Bar:
    ...
@overload
def Add(this: Baz, that: Bar) -> 'Optional[Baz]':
    ...
def Add(this, that):
    if this is None or that is None:
        return None
    else:
        return this + that

这使我怀疑“重叠”是针对NoneType的,但是感觉应该可以解决,我完全偏离了基础吗?

编辑

所以,我真的只是在这儿闲逛,但是我想,当两个参数都为None时,这肯定是模棱两可的,我希望可以通过以下方法解决它:

@overload
def Add(this: None, that: None) -> None:
    ...
@overload
def Add(this: Optional[Foo], that: Optional[Foo]) -> Optional[Foo]:
    ...
@overload
def Add(this: Optional[Bar], that: Optional[Bar]) -> Optional[Bar]:
    ...
@overload
def Add(this: Optional[Baz], that: Optional[Bar]) -> Optional[Baz]:
    ...
def Add(this, that):
    if this is None or that is None:
        return None
    else:
        return this + that

但是我仍然得到:

mcve4.py:37: error: Overloaded function signatures 2 and 3 overlap with incompatible return types
mcve4.py:37: error: Overloaded function signatures 2 and 4 overlap with incompatible return types
mcve4.py:40: error: Overloaded function signatures 3 and 4 overlap with incompatible return types

Edit2 沿着同一个花园小径,我尝试了以下方法:

@overload
def Add(this: None, that: None) -> None:
    ...
@overload
def Add(this: Foo, that: Optional[Foo]) -> Optional[Foo]:
    ...
@overload
def Add(this: Optional[Foo], that: Foo) -> Optional[Foo]:
    ...
@overload
def Add(this: Baz, that: Bar) -> Optional[Baz]:
    ...
@overload
def Add(this: Baz, that: Optional[Bar]) -> Optional[Baz]:
    ...
@overload
def Add(this: Optional[Baz], that: Bar) -> Optional[Baz]: # 6
    ...
@overload
def Add(this: Bar, that: Optional[Bar]) -> Optional[Bar]:
    ...
@overload
def Add(this: Optional[Bar], that: Bar) -> Optional[Bar]: # 8
    ...

def Add(this, that):
    if this is None or that is None:
        return None
    else:
        return this + that

我现在得到:

mcve4.py:49: error: Overloaded function signatures 6 and 8 overlap with incompatible return types

这开始对我有意义,我认为从根本上我想做的事是不安全/已损坏的。我可能不得不用另一种方式切掉gordian结。

1 个答案:

答案 0 :(得分:4)

恐怕没有一种特别干净的方法可以解决这个问题,至少我个人没有意识到。如您所见,您的类型签名包含mypy不允许的基本模糊性:如果尝试使用类型为Add的参数调用None,mypy基本上将无法推断出哪个给定的重载变量匹配。

有关此问题的更多讨论,请参见checking overload invariants上的mypy文档-搜索讨论“本质上不安全地重叠的变体”的段落,然后从那里开始阅读。


但是,在这种特殊情况下,我们可以通过拼写重载以更精确地匹配实际运行时行为来自由摆动。特别是,我们拥有一个很好的属性,如果任一参数为“ None”,我们必须也返回无。如果我们对此进行编码,则mypy最终会得到满足:

@overload
def Add(this: None, that: None) -> None:
    ...
@overload
def Add(this: Foo, that: None) -> None:
    ...
@overload
def Add(this: Bar, that: None) -> None:
    ...
@overload
def Add(this: Baz, that: None) -> None:
    ...
@overload
def Add(this: None, that: Foo) -> None:
    ...
@overload
def Add(this: None, that: Bar) -> None:
    ...
@overload
def Add(this: Foo, that: Foo) -> Foo:
    ...
@overload
def Add(this: Bar, that: Bar) -> Bar:
    ...
@overload
def Add(this: Baz, that: Bar) -> Baz:
    ...
def Add(this, that):
    if this is None or that is None:
        return None
    else:
        return this + that

x: Optional[Baz]
y: Optional[Bar]

reveal_type(Add(x, y))  # Revealed type is 'Union[Baz, None]'

这种工作原理一开始似乎令人惊讶-毕竟,我们传入了Optional[...]类型的参数,但是没有一个重载包含该类型!

mypy在这里做的事情被非正式地称为“联合数学”-它基本上观察到xy分别是类型Union[Baz, None]Union[Bar, None]的并集,并尝试做一个嵌套的for循环的精神等效项,以检查这些结合的每个可能组合。因此,在这种情况下,它检查匹配(Baz, Bar)(Baz, None)(None, Bar)(None, None)的重载变量,并返回类型为Baz,None,None,和无。

最后的返回类型是这些值的联合:Union[Baz, None, None, None]。简化为Union[Baz, None],这是所需的返回类型。


当然,此解决方案的主要缺点是,它非常冗长-可能在一个难以忍受的程度,具体取决于您拥有的这些帮助程序功能的数量以及此“我们可能返回无”问题的普遍性代码库。

在这种情况下,您可能要做的就是在整个代码库中普遍使用“无”声明“破产”,并开始使用"strict optional" mode disabled运行mypy。

简而言之,如果您使用--no-strict-optional标志运行mypy,则指示mypy假定'None'是 every 类的有效成员。这与Java假定'null'是每种​​类型的有效成员的方式相同。 (嗯,每种非基本类型,但无论如何)。

这会弱化代码的类型安全性(有时会大大降低),但是它会 使您简化代码,使其看起来像这样:

class Foo:
    value: int
    def __init__(self, value: int) -> None:
        self.value = value

    # Note: with strict-optional disabled, returning 'Foo' vs
    # 'Optional[Foo]' means the same thing
    def __add__(self, other: 'Foo') -> Foo:
        result = self.value - other.value
        if result > 42:
            return None
        else:
            return Foo(result)


@overload
def Add(this: Foo, that: Foo) -> Foo:
    ...
@overload
def Add(this: Bar, that: Bar) -> Bar:
    ...
@overload
def Add(this: Baz, that: Bar) -> Baz:
    ...
def Add(this, that):
    if this is None or that is None:
        return None
    else:
        return this + that

x: Optional[Baz]
y: Optional[Bar]

reveal_type(Add(x, y))  # Revealed type is 'Baz'

严格来说,过载检查应报告“不安全重叠”错误,其原因与启用严格可选时返回的原因相同。但是,如果这样做,则在禁用strict-optional时重载将完全不可用:因此,mypy故意削弱此处的检查并忽略该特定错误情况。

此模式的主要缺点是您现在不得不执行更多的运行时检查。如果您收到某个类型为Baz的值,则它实际上可能为None-与Java中的任何对象引用实际上可以为null相似。

在您的情况下,这可能是个不错的选择,因为您已经将这些类型的运行时检查分散在了各处。

如果您认同“零是十亿美元的错误”的思想流派,并且想生活在严格可选的世界中,则可以使用的一种方法是逐步重新启用严格选择的部分内容。使用mypy config file来创建您的代码库。

基本上,您可以通过config文件在每个模块的基础上配置许多(尽管不是全部)mypy选项,如果您尝试向现有的代码库中添加类型并找到过渡的地方,这可能非常方便一下子就很难了。从松散的全局设置开始,然后逐渐使它们变得越来越严格。


如果这两个选项都感觉太极端了(例如,您不想在任何地方从上方添加详细的签名,但又不想放弃严格的可选选项),那么您可以做的最后一个选择就是简单地通过向每行添加一个# type: ignore来使错误 silence 错误。mypy报告了“不安全重叠的类型”错误。

这在某种程度上也是失败的,但可能是更局限的。甚至typeshed是标准库的类型提示存储库,在这里和那里都包含一些分散的# type: ignore注释,用于某些使用PEP 484类型无法表达的功能。

这是否可行,取决于您的具体情况。如果您分析代码库,并认为可以忽略潜在的不安全因素,那么也许这可能是最简单的方法。