如何安全地键入接受通用容器类的函数?

问题描述 投票:0回答:1
from __future__ import annotations

import logging
from datetime import datetime, UTC
from typing import Any, Generic, Self, Protocol, TypeVar

from pydantic import AwareDatetime, BaseModel

logger = logging.getLogger(__name__)
EventDataT_co = TypeVar('EventDataT_co')


class Event(BaseModel, Generic[EventDataT_co]):
    raised_at: AwareDatetime
    data: tuple[EventDataT_co, ...]

    @classmethod
    def from_data(cls, *data: EventDataT_co) -> Self:
        return cls(raised_at=datetime.now(UTC), data=data)


class EventRepository(Protocol):
    def save_events_unsafe(self, *events: Event[Any]) -> None:  # same as `*events: Event[Any]`
        """same"""

    def save_events_safe(self, *events: Event[object]) -> None:
        """Only allowed if Event is covariant, but then I can't have a custom constructor"""

    def save_events_gen(self, *events: Event[EventDataT_co]) -> None:
        """It implies this is a generic function, when it's more `.data`-agnostic"""


class InheritedEventRepository(EventRepository):
    def save_events_unsafe(self, *events: Event[Any]) -> None:
        logger.info(str([data.id for event in events for data in event.data]))  # Type-checker says ok, dev made an uncaught mistake

    def save_events_safe(self, *events: Event[object]) -> None:
        logger.info(str([data.id for event in events for data in event.data]))  # Type-checker says error: "object" has no attribute "id"  [attr-defined]

    def save_events_gen(self, *events: Event[EventDataT_co]) -> None:
        logger.info(str([data.id for event in events for data in event.data]))
        # Type-checker error: "EventDataT_co" has no attribute "id"  [attr-defined] if covariant
        # No error if invariant


event_1 = Event.from_data(1)
event_2 = Event.from_data('foo')
from typing import reveal_type
print(reveal_type(event_1))  # Event[builtins.int]
print(reveal_type(event_2))  # Event[builtins.str]
InheritedEventRepository().save_events_unsafe(event_1, event_2)
InheritedEventRepository().save_events_safe(event_1, event_2)
# error: Argument 1 to "save_events_safe" of "InheritedEventRepository" has incompatible type "Event[int]"; expected "Event[object]"  [arg-type] if invariant
# No error if covariant
InheritedEventRepository().save_events_gen(event_1, event_2)
# error: Cannot infer type argument 1 of "save_events_gen" of "InheritedEventRepository"  [misc] if invariant
# No error if covariant

Mypy 抛出:

错误:无法使用协变类型变量作为参数[杂项]

def from_data(...)
构造函数行上。我不想使
Event
协变,但这似乎是允许
save_events_safe
被 mypy 接受的唯一方法?我不想在
Event[Any]
中使用
save_events
,因为
save_events
将被子类化(我不希望继承它的开发人员具有 没有类型检查安全性)。最后,
save_events_gen(self, *events: Event[EventDataT_co])
save_events_safe
有同样的问题(仅协变)

这意味着我要么坚持使用一个协变版本,不允许自定义构造函数(而且,Event确实不应该在这种情况之外协变使用),要么是一个不变的版本,迫使我使用 Any不可知函数。我该如何解决这个问题?

python generics mypy python-typing
1个回答
0
投票

这是我自己也遇到过的事情,让我们陈述一下事实:

  • 事件是通用的 - 有道理,它是一些具有通用属性的类,可以被库用户覆盖
  • 我们想要一个使用 Event 的函数,但它不关心它的属性在通用属性之外。

此代表:

def save_events(*events: Event[T]): ...

满足所有这些条件 - 如果您将通用视为“独立于某些内部部分的属性”,那么“数据不可知”和“通用”实际上是同一件事。

但是协方差问题呢?

以下代码段:

from __future__ import annotations
from datetime import datetime, UTC
from typing import  Generic, Self, TypeVar
from pydantic import AwareDatetime, BaseModel


EventDataT = TypeVar('EventDataT')

class Event(BaseModel, Generic[EventDataT]):
    raised_at: AwareDatetime
    data: tuple[EventDataT, ...]

    @classmethod
    def from_data(cls, *data: EventDataT) -> Self:  
        return cls(raised_at=datetime.now(UTC), data=data)


EventDataT_co = TypeVar('EventDataT_co', covariant=True)


def save_events(*events: Event[EventDataT_co]): ...

class Test:
    pass

save_events(Event[Test].from_data(Test()))

...在pyright和mypy严格模式下类型检查对我来说都很好。通过引入一种新的类型 var,我们可以使一个为协变,而另一个则为非协变。希望这有帮助!

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