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"

为什么?

"无效类型"错误的原因是您试图创建泛型类而不是泛型函数。也就是说,您正试图创建一个可以作为一个整体存储一些泛型数据的类,而不是让单个函数或方法成为泛型。

对此的表面修复方法是只修复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]。因此,在这种情况下,不需要注释。

最新更新