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

问题描述 投票:0回答:1

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

这些是我的课程:

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"

这是为什么?

python python-typing mypy
1个回答
2
投票

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

对此问题的表面解决方法是修复您的 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
call-sites来执行此操作:

@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
尝试附加非文本消息怎么办?

因此,我们可以做的是向 mypy 承诺

messages

永远不会被用作

List[TextMessage]
,而是始终被用作
List[GenericMessage]
——这使得类型对齐,保证后续代码可以'不要滥用你的列表,并满足我的要求。

请注意,如果您尝试向列表中添加更多消息类型,则无需添加此注释。例如,假设您将“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]
。所以在这种情况下,不需要注释。
    

© www.soinside.com 2019 - 2024. All rights reserved.