我尝试使用
@overload
来传达调用函数的不同方式,但是在代码中使用简单的 else
语句轻松传达的内容在类型注释中是不可能的。 如果没有“else”,MyPy(正确地)会抱怨重载版本不匹配(例如,请参阅下面的代码片段)。
error: Overloaded function signatures 1 and 2 overlap with incompatible return types
我是否理解正确,这个问题没有好的解决方案?
例如。这是一个简单的例子:
ListOrTuple = TypeVar("ListOrTuple", List, Tuple)
# unfortunately, typing doesn't support "anything else" at the moment
# https://github.com/python/typing/issues/599#issuecomment-586007066
AnythingElse = TypeVar("AnythingElse")
# what I would like to have is something like AnythingElse= TypeVar("AnythingElse", Not[List,Tuple])
@overload
def as_list(val: ListOrTuple) -> ListOrTuple:
...
@overload
def as_list(val: AnythingElse) -> List[AnythingElse]:
...
def as_list(val):
"""Return list/tuple as is, otherwise wrap in a list
>>> as_list("test")
['test']
"""
return val if isinstance(val, (list, tuple)) else [val]
这个问题已经足够老了,但是这个问题在多个问题中出现。这是一个重复的目标,因为问题是以通用且可重用的方式制定的。其他询问类型为“X 或非 X”的重载的问题可以作为此问题的欺骗而被关闭。
这个与重载相关的问题(表达类型
not X
或 Any \ X
)并未计划用于任何 mypy
里程碑。
首先,解决方案:只需像您所做的那样编写两个重载即可,例如(我减少了导入和整体复杂性以简化示例)
from typing import Any, TypeVar
ListOrTuple = TypeVar("ListOrTuple", list[Any], tuple[Any, ...])
AnythingElse = TypeVar("AnythingElse")
@overload
def as_list(val: ListOrTuple) -> ListOrTuple: ... # type: ignore[misc]
@overload
def as_list(val: AnythingElse) -> list[AnythingElse]: ...
...这按预期工作。有关重叠签名的错误消息更像是 lint 警告,而不是真实类型错误 [1-2]。您所需要做的就是忽略错误行,如上面的示例所示。一切都会很好。 几乎:请参阅下面的详细信息。
这在mypy官方文档中也有解释。
简而言之([4]):
当调用重载函数时,
mypy
会检查每个重载并检查是否存在匹配。如果传递给函数的参数与重载参数的类型兼容,则 mypy
选择此重载并将其作为常规函数进一步处理(如果涉及 TypeVar
,事情会变得更加困难 - 事实上 mypy
尝试替换仅当此过程成功时才会选择重载)。在这里,我们认为 A
与 B
兼容,当且仅当 A
是 B
的(名义或结构,非严格)子类型。这对我们意味着什么?如果有多个匹配项,则将使用第一个匹配项。因此,要说 (int) -> str; (float which is not int) -> float
,我们必须按此顺序定义两个重载 (int) -> str; (float) -> float
。你快乐吗?我不是,mypy
未能将 int
视为重载中的 float
子类型,这是一个已报告的错误。
当重载定义被
mypy
解析和解释时,它会验证其形式正确性。如果参数不重叠,则重载是可以的。否则,参数类型越窄,返回类型应该越窄 - 否则 mypy
抱怨。这里可能会发出两个不同的错误(数字可能会随着更多签名而不同):
Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader
。这是一个坏兆头:你的第二次过载完全被忽略了。基本上这意味着您需要交换签名才能使两者都工作并收到另一条消息: Overloaded function signatures 1 and 2 overlap with incompatible return types
。这好多了!基本上它说“你确定这是你想要的吗?严格来说,如果没有特定于实现的假设,任何重载都可以由 PEP484 兼容的类型检查器选择,而且还有一些边缘情况!”。您可以放心地说“是的,我是这个意思!”并忽略该消息。但要小心(见下文)!好吧,我在上面说“一切都会好起来”时有点作弊了。主要问题是你可以声明一个变量具有更宽的类型。如果返回类型不兼容,那么您实际上已经调整了类型检查器以产生一些与运行时行为不匹配的输出。像这样:
class A: pass
class B(A): pass
@overload
def maybe_ok_1(x: B) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types
@overload
def maybe_ok_1(x: A) -> str: ...
def maybe_ok_1(x): return 0 if isinstance(x, B) else 'A'
# So far, so good:
reveal_type(maybe_ok_1(B())) # N: revealed type is "builtins.int"
reveal_type(maybe_ok_1(A())) # N: revealed type is "builtins.str"
# But this can be dangerous:
# This is `B`, and actual return is `int` - but `mypy` doesn't think so.
x: A = B()
reveal_type(maybe_ok_1(x)) # N: Revealed type is "builtins.str" # Ooops!
请注意,这不限于“手动”向上转换,因为您的参数可能会传递给接受更广泛类型的函数:
# cont. previous snippet
def do_work(x: A) -> str:
return maybe_ok_1(x) + "foo"
do_work(B()) # Oops, this raises a TypeError
最后,看几个例子来了解一些实际的观点:
class A: pass
class B(A): pass
# Absolutely fine, no overlaps
@overload
def ok_1(x: str) -> int: ...
@overload
def ok_1(x: int) -> str: ...
def ok_1(x): return '1' if isinstance(x, int) else 1
reveal_type(ok_1(1))
reveal_type(ok_1('1'))
# Should use `TypeVar` instead, but this is a synthetic example - forgive me:)
@overload
def ok_2(x: B) -> B: ...
@overload
def ok_2(x: A) -> A: ...
def ok_2(x): return x
reveal_type(ok_2(A())) # N: revealed type is "__main__.A"
reveal_type(ok_2(B())) # N: revealed type is "__main__.B"
# But try to reverse the previous example - it is much worse!
@overload
def bad_1(x: A) -> A: ...
@overload
def bad_1(x: B) -> B: ... # E: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader [misc]
def bad_1(x): return x
reveal_type(bad_1(B())) # N: Revealed type is "__main__.A" # Oops! Though, still true
reveal_type(bad_1(A())) # N: Revealed type is "__main__.A"
# Now let's make it completely invalid:
@overload
def bad_2(x: A) -> int: ...
@overload
def bad_2(x: B) -> str: ... # E: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader [misc]
def bad_2(x): return 'B' if isinstance(x, B) else 1
reveal_type(bad_2(B())) # N: Revealed type is "builtins.int" # Oops! The actual return is 'B'
reveal_type(bad_2(A())) # N: Revealed type is "buitlins.int"
# Now watch something similar to ok_2, but with incompatible returns (we may want to ignore defn line)
@overload
def maybe_ok_1(x: B) -> int: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types
@overload
def maybe_ok_1(x: A) -> str: ...
def maybe_ok_1(x): return 0 if isinstance(x, B) else 'A'
# So far, so good:
reveal_type(maybe_ok_1(B())) # N: revealed type is "builtins.int"
reveal_type(maybe_ok_1(A())) # N: revealed type is "builtins.str"
# But this can be dangerous:
# This is `B`, and actual return is `int` - but `mypy` doesn't think so.
x: A = B()
reveal_type(maybe_ok_1(x)) # N: Revealed type is "builtins.str" # Ooops!
这是 playground 链接,因此您可以调整代码以查看可以更改的内容。
来自
mypy
问题跟踪器的参考:
使用
bound="Union[List, Tuple]"
:
from typing import Any, List, Tuple, TypeVar, Union, overload
ListOrTuple = TypeVar("ListOrTuple", bound="Union[List, Tuple]")
AnythingElse = TypeVar("AnythingElse")
@overload
def as_list(val: ListOrTuple) -> ListOrTuple:
pass
@overload
def as_list(val: AnythingElse) -> List[AnythingElse]:
pass
def as_list(val: Any) -> Any:
"""Return list/tuple as is, otherwise wrap in a list
>>> as_list("test")
['test']
"""
return val if isinstance(val, (list, tuple)) else [val]
a = as_list(2) # it's List[int]
b = as_list('2') # it's List[str]
c = as_list(['2', '3']) # it's List[str]
这是我的解决方法。它对我来说足够好,但我一点也不喜欢它。
# attempt to list all the "other" possible types
AnythingElse = TypeVar("AnythingElse", Set, Mapping, type, int, str, None, Callable, Set, Deque, ByteString)
ListOrTuple = TypeVar("ListOrTuple", List, Tuple, Sequence)
@overload
def as_list(val: ListOrTuple) -> ListOrTuple:
...
@overload
def as_list(val: AnythingElse) -> List[AnythingElse]:
...
def as_list(val):
"""Return list/tuple as is, otherwise wrap in a list
>>> as_list("test")
['test']
"""
return val if isinstance(val, (list, tuple)) else [val]