mypy 允许在特定行重新定义

mypy allow redefinition at a specific line

我正在尝试使用 mypy 整理我的 python 代码。我依赖的一个实用程序使用一个参数可以采用多种类型的回调。类型通常由附带的主题字符串定义。

我遇到了一个问题,回调需要多个主题,每个主题都有唯一的类型。在这种情况下,我不确定如何与 mypy 通信,该类型比两者的联合更受限制。

def callback(self, topic: str, msg: Union[A, B]):
    if topic == "a":
        msg: A
        # Do something specific to topic "a"
    if topic == "b":
        msg: B
        # Do something specific to topic "b"

我试过写成 msg: A # type: ignore,但是 mypy 忽略了这一行。由于该实用程序的结构,我也无法使用 isinstance

可能的解决方案

我认为有两件事应该可行,但我不确定 mypy 是否具有这样的功能:

  1. 允许在特定行上重新定义。当 运行 --allow-redefinition 错误被抑制。不幸的是,运行 在我看来,整个包裹太松了,所以我宁愿避免它。
  2. 将变量的类型绑定到另一个变量的值。我不确定它是否存在,但它可能类似于 TypedDict,其中键可以绑定到不同类型的值,而不是两个单独的变量。

这需要一些恼人的样板代码和一些技巧,但您可以使用 PEP 647 TypeGuard 来完成。

TypeGuard 的存在是为了告诉 MyPy 某些自定义函数能够以类似于 isinstance 的方式进行类型缩小。例如,您可能有一个接受列表的函数,如果该列表中的所有内容都是整数,则 returns 为真:

def are_all_ints(probe: List[Any]) -> TypeGuard[List[int]]:
    return all(isinstance(int, p) for p in probe)

xs: List[Any] = ...

if are_all_ints(xs):
    # MyPy trusts the check was done correctly and therefore
    reveal_type(xs) # this gives List[int] within this section
    

如您所见,TypeGuard 对传递给它们的变量进行操作。如果将 topic 字符串传递给合适的类型保护函数,则可能会变窄,但不会变窄 msg。解决方法是将它们打包到一个临时变量中。

def callback(self, topic: str, msg: Union[A, B]):
    
    def is_callback_from_a(p: Tuple[str, Union[A, B]]) -> TypeGuard[Tuple[Literal["a"], A]]:
        return p[0] == "a"
    
    def is_callback_from_b(p: Tuple[str, Union[A, B]]) -> TypeGuard[Tuple[Literal["b"], B]]:
        return p[0] == "b"
    
    # Package the two parameters into one variable so that 
    # the guard clears them simulataneously
    topic_msg_pair = (topic, msg)
    
    if is_callback_from_a(topic_msg_pair):
        ...
        # topic_msg_pair[1] is known to be an A here
    elif is_callback_from_b(topic_msg_pair):
        ...
        # topic_msg_pair[1] is known to be a B here

Overload signatures 将使您的函数在外部正确,即使您必须在内部进行转换。

class Example:

    @overload
    def callback(self, topic: Literal["a"], msg: A) -> None: ...
    @overload
    def callback(self, topic: Literal["b"], msg: B) -> None: ...
    def callback(self, topic: str, msg: Union[A, B]):
        if topic == "a":
            a_msg = cast(A, msg)
            reveal_locals()
        if topic == "b":
            b_msg = cast(B, msg)
            reveal_locals()

(注意:Literal 已添加到 Python 3.8 中的 typing。如果您使用的是早期版本,可以从 typing_extensions 获取)

在函数内部,我们仍然必须显式转换,但在外部,调用者将始终需要使用 (1) 文字“a”和 A 或 (2 ) 文字 "b" 和 a B.