mypy和attrs:对子类列表进行类型检查时出错

时间:2018-11-15 09:31:07

标签: mypy

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

这些是我的课程:

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"

那是为什么?

1 个答案:

答案 0 :(得分:1)

出现“无效类型”错误的原因是因为您试图创建generic class而不是generic function。也就是说,您正在尝试创建一个可以整体存储一些通用数据的类,而不仅仅是使单个函数或方法具有通用性。

对此的表面修复是仅修复您的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的类型为List[TextMessage]。由于我在先前对您的答复中说明的原因,例如将其传递到期望List[GenericMessage]的可写容器中是不正确的。如果MessageContainer尝试附加非TextMessage的消息怎么办?

因此,我们可以做的是向mypy保证messages永远不会用作List[TextMessage],而总是会用作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]。因此,在这种情况下,不需要注释。