代码之家  ›  专栏  ›  技术社区  ›  juanpa.arrivillaga

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

  •  8
  • juanpa.arrivillaga  · 技术社区  · 7 年前

    那么,让我们从一个例子开始。假设我们有几种类型可以组合在一起,假设我们正在使用 __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

    注意,如果我移除 可选的 疯狂,我的孩子没有抱怨。我甚至可以任选一个!:

    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
    

    这让我怀疑“重叠”是非类型的,但感觉这应该是可解决的,我是完全脱离了基础吗?

    编辑

    所以,我真的只是在这里徘徊,但我想,当两个论点都是 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
    

    编辑2 沿着同一条花园小径,我尝试了以下方法:

    @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
    

    这对我来说已经开始有意义了,我认为从根本上说,我试图做的是不安全/不安全的。我可能要用另一种方式来切断戈尔迪安的结…

    1 回复  |  直到 7 年前
        1
  •  4
  •   Michael0x2a    7 年前

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

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


    但是,在这种特定的情况下,我们可以通过拼写出更精确地匹配实际运行时行为的重载来自由摆动。特别是,我们有一个很好的属性,如果任何一个参数是“无”,我们 必须 也不返回。如果我们对此进行编码,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在这里做的是非正式的“联合数学”——它基本上观察到 x y 两种类型的联合 Union[Baz, None] Union[Bar, None] 分别尝试做一个嵌套的for循环的精神等价物来检查这些联合的每一个可能的组合。因此,在本例中,它检查是否有匹配的重载变量 (Baz, Bar) , (Baz, None) , (None, Bar) (None, None) 并分别返回baz、none、none和none类型的返回值。

    最后的返回类型是这些值的并集: Union[Baz, None, None, None] . 这简化为 工会[BAZ,无] ,这是所需的返回类型。


    当然,这个解决方案的主要缺点是,它非常冗长——可能在一个无法忍受的程度上,这取决于您有多少助手函数,以及这个“我们可能不会返回任何”问题在代码库中的普遍性。

    如果是这样的话,您可以做的事情就是在代码库中普遍存在“无”的情况下宣布“破产”,并开始运行mypy "strict optional" mode disabled .

    简而言之,如果你用 --no-strict-optional flag,你在指示mypy假设“none”是 每一个 班级。这与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'
    

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

    这种模式的主要缺点是您现在被迫进行更多的运行时检查。如果你收到一些类型的值 Baz ,实际上可能是 没有 ——类似于Java中的任何对象引用实际上可能如何 null .

    在您的案例中,这可能是一个不错的折衷方案,因为您已经将这些类型的运行时检查分散到了所有地方。

    如果您订阅了“空是一个十亿美元的错误”的思想流派,并且希望生活在严格的可选世界中,您可以使用的一种技术是通过使用 mypy config file .

    基本上,您可以通过配置文件在每个模块的基础上配置许多(尽管不是全部)mypy选项,如果您试图将类型添加到预先存在的代码库中,并发现一次性转换是很难处理的,那么这非常方便。从松散的全局设置开始,然后随着时间的推移逐步使它们更严格。


    如果这两个选项都过于极端(例如,您不想从上面到处添加详细签名,但也不想放弃严格的可选选项),那么您可以做的最后一个选项就是 沉默 通过添加 # type: ignore Mypy向每一行报告“不安全的重叠类型”错误。

    这在某种程度上也是失败,但可能是一种更本地化的失败。偶数 typeshed 是标准库的类型提示库,包含一些分散的 #类型:忽略 使用PEP 484类型时,某些函数无法表达的注释。

    这是否是一个解决方案,你可以取决于你的具体情况。如果您分析代码库,并认为潜在的不安全性是您可以忽略的,也许这是最简单的前进方式。