繁体   English   中英

Python 带有排除类型的 TypeVar 的类型注释

[英]Python type annotations with TypeVar that excludes types

我正在尝试使用@overload来传达调用 function 的不同方式,但是在代码中使用简单的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]

这是我的解决方法。 它对我来说足够好,但我根本不喜欢它。

# 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]

使用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]

这个问题够老了,但是这个问题出现在多个问题中。 这是一个重复的目标,因为问题是以通用和可重用的方式制定的。 询问有关类型为“X or not X”的重载的其他问题可以作为此问题的愚弄而关闭。

此问题(表示类型not XAny \ 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]):

  • 当调用重载 function 时, mypy遍历每个重载并检查是否存在匹配项。 如果传递给 function 的 arguments 与重载参数的类型兼容, mypy选择此重载并将其作为常规 function 进一步处理(如果涉及TypeVar ,事情会变得更加困难 - 实际上mypy尝试替换它们并仅选择重载如果此过程成功)。 在这里,当且仅当AB的(名义上或结构上的,非严格的)子类型时,我们才认为AB兼容。 这对我们意味着什么? 如果有多个匹配项,将使用第一个。 这样说(int) -> str; (float which is not int) -> float (int) -> str; (float which is not int) -> float我们必须定义两个重载, (int) -> str; (float) -> float (int) -> str; (float) -> float按此顺序浮动。 你快乐吗? 我不是, mypy未能在重载中将int视为float子类型,这是一个已报告的错误。

  • mypy解析和解释重载定义时,它会验证其形式上的正确性。 如果 arguments 没有重叠,重载是可以的。 否则,参数类型越窄,返回类型应该越窄——否则mypy抱怨。 这里可能会发出两个不同的错误(数字可能会因更多签名而不同):

    1. Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader 这是一个不好的迹象:您的第二次过载完全被忽略了。 基本上这意味着您需要交换签名以使两者都起作用并改为获取另一条消息:
    2. Overloaded function signatures 1 and 2 overlap with incompatible return types 这样好很多? 基本上它说“你确定它是你想要的吗,严格来说,没有特定于实现的假设,PEP484 兼容的类型检查器可以选择任何重载。还有一些边缘情况,”。 你可以放心地说“是的,我是说这个!” 并忽略该消息。 但要小心(见下文)!

为什么错误消息在那里?

好吧,我在上面说“一切都会好的”时有点作弊。 主要问题是您可以声明一个具有更宽类型的变量。 如果返回类型不兼容,那么您实际上已经调整了类型检查器以生成一些 output 与运行时行为不匹配。 像这样:

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!

例子

最后,查看一些示例以了解一些实际观点:


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!

这是游乐场链接,因此您可以调整代码以查看可以更改的内容。

来自mypy问题跟踪器的参考资料:

  1. 只是 go 在 #12759
  2. 只是 go 在 #13805
  3. 特殊情况还不足以打破规则吗? 但他们仍然保持开放......
  4. 读我! 这包含对overload实现的简短解释
  5. 订购很重要!

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM