所以,让我们从一个例子开始。假设我们有几种可以组合在一起的类型,假设我们使用__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结。
答案 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在这里做的事情被非正式地称为“联合数学”-它基本上观察到x
和y
分别是类型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类型无法表达的功能。
这是否可行,取决于您的具体情况。如果您分析代码库,并认为可以忽略潜在的不安全因素,那么也许这可能是最简单的方法。