首页 > 解决方案 > mypy 和 attrs:错误类型检查子类列表

问题描述

我有一个可以包含不同类型消息的消息容器。目前,只有短信。

这些是我的课:

from typing import List, TypeVar

import attr


@attr.s(auto_attribs=True)
class GenericMessage:
    text: str = attr.ib()


GMessage = TypeVar('GMessage', bound=GenericMessage)


@attr.s(auto_attribs=True)
class TextMessage(GenericMessage):

    comment: str = attr.ib()


@attr.s(auto_attribs=True)
class MessageContainer:

    messages: List[GMessage] = attr.ib()

    def output_texts(self):
        """ Display all message texts in the container """
        for message in self.messages:
            print(message.text)

这个想法是消息不仅可以接受文本消息,还可以接受任何其他消息,所有这些都共享GenericMessage将由容器使用的相同协议。

因此,在进行类型检查时,mypy会在此用法中显示错误:

messages = [
    TextMessage(text='a', comment='b'),
    TextMessage(text='d', comment='d')
]


container = MessageContainer(messages=messages)
container.output_texts()

错误是:

error: Invalid type "GMessage"

这是为什么?

标签: pythonmypy

解决方案


“无效类型”错误的原因是因为您试图创建泛型类而不是泛型函数。也就是说,您尝试创建一个可以作为一个整体存储一些通用数据的类,而不是只制作单个函数或方法。

对此的表面修复只是修复您的 MessageContainer 类,使其具有适当的通用性,如下所示:

from typing import Generic

# ...snip...

@attr.s(auto_attribs=True)
class MessageContainer(Generic[GMessage]):

    messages: List[GMessage] = attr.ib()

    def output_texts(self) -> None:
        """ Display all message texts in the container """
        for message in messages:
            print(message.text)

这将最终解决您上面描述的错误。

但是,这可能不是您想要使用的解决方案——问题在于,您没有创建一个可以包含多种不同类型消息的 MessageContainer,而是创建了一个可以参数化为特定类型方法的 MessageContainer。

您可以通过添加对reveal_types(...)伪函数的调用来亲眼看到这一点:

messages = [
    TextMessage(text='a', comment='b'),
    TextMessage(text='d', comment='d'),
]

container = MessageContainer(messages=messages)
reveal_type(container)

(无需reveal_types从任何地方导入——mypy 是该功能的特殊情况)。

如果您对此运行 mypy,它将报告container类型为MessageContainer[TextMessage]. 这意味着您的容器将来将无法接受任何其他类型的消息。也许这是您想要做的,但根据您上面的描述,可能不是。


我建议改为做以下两件事之一。

如果您的 MessageContainer 是只读的(例如,在您构建它之后,您不能再向其中添加新消息),只需切换到使用 Sequence。如果您的自定义数据结构是只读的,那么在内部也可以使用只读的东西:

@attr.s(auto_attribs=True)
class MessageContainer:

    messages: Sequence[GenericMessage] = attr.ib()

    def output_texts(self) -> None:
        """ Display all message texts in the container """
        for message in messages:
            print(message.text)

如果您确实想让您的 MessageContainer 可写(例如,可能添加一个add_new_message方法),我建议您实际修复调用站点MessageContainer执行此操作:

@attr.s(auto_attribs=True)
class MessageContainer:

    messages: List[GenericMessage] = attr.ib()

    def output_texts(self) -> None:
        """ Display all message texts in the container """
        for message in messages:
            print(message.text)

    def add_new_message(self, msg: GenericMessage) -> None:
        self.messages.append(msg)

# Explicitly annotate 'messages' with 'List[GenericMessage]'
messages: List[GenericMessage] = [
    TextMessage(text='a', comment='b'),
    TextMessage(text='d', comment='d'),
]

container = MessageContainer(messages=messages)

通常, mypy 推断它messages是 type List[TextMessage]。将它传递到一个期望 a 的可写容器中List[GenericMessage]是不合理的,原因我在之前的回答中解释过——例如,如果MessageContainer尝试附加不是 TextMessage 的消息怎么办?

所以,我们可以做的是向 mypy 承诺,它messages永远不会被用作 a List[TextMessage],而是总是被用作a——List[GenericMessage]这使得类型对齐,保证后续代码不会滥用你的列表,并满足 mypy。

请注意,如果您尝试向列表中添加更多消息类型,则不需要添加此注释。例如,假设您在列表中添加了“VideoMessage”类型:

messages = [
    TextMessage(text='a', comment='b'),
    TextMessage(text='d', comment='d'),
    VideoMessage(text='a', link_to_video='c'),
]

container = MessageContainer(messages=messages)

在这种情况下,mypy 会检查 的内容messages,发现它包含多个 GenericMessage 的子类,因此推断出最合理的类型messages可能是List[GenericMessage]。所以在这种情况下,不需要注释。


推荐阅读