Как безопасно ввести функцию, которая принимает универсальный класс контейнера?

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]`
        """not type-safe (someone might make unsafe assumption on `.data`)"""

    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 для агностические функции. Как мне это решить?

Если класс, в котором определен save_events, также является универсальным (и ему требуется Event[T]), решает ли это проблему?

Samwise 24.06.2024 02:27

В результате этот класс станет Generic[Event[T]], то есть экземпляр этого класса может принимать только один конкретный тип события (например: Event[Foo]). Это не сработает. Класс является агностическим, а не универсальным. По сути, мне следует использовать Event[Any], но это отключит проверку типов, и подкласс может случайно получить незаконный доступ к атрибуту. Следовательно Event[object] :/

takeshi2010 24.06.2024 03:29

Возможно, вам поможет расширить пример, чтобы продемонстрировать проблему, которая возникает, если вы используете Any. Мне неясно, как и почему этому классу вообще разрешен доступ data, если он должен быть полностью независимым от своего типа.

Samwise 24.06.2024 05:07

Доступ к атрибутам данных запрещен, но он может получить доступ к данным. В том-то и дело, что я не могу контролировать, что будет делать другой разработчик при наследовании этого класса, если я использую Any. object предотвращает этот тип доступа. (Если это поможет понять, я создаю библиотеку, которая будет использоваться другими разработчиками).

takeshi2010 24.06.2024 10:48

Что значит «доступ» к данным, но не к их атрибутам??? Опять же, пример кода был бы очень полезен.

Samwise 24.06.2024 16:38

Доступ к данным: event.data -> легально / Доступ к атрибуту данных: event.data.id -> незаконно. Надеюсь, это прояснит вам ситуацию.

takeshi2010 24.06.2024 19:41

Вчера было мало времени, извини. Я обновил код. Надеюсь, это прояснит проблему. Как указано в моем исходном вопросе (он был отредактирован), я не очень хорошо разбираюсь в вариациях дженериков. Я читал документацию, я понимаю, но мне нелегко жонглировать этой концепцией. Последние 4 дня я потратил на создание небольшого репозитория, совместимого с mypy, и это был ад (особенно потому, что это библиотека, ориентированная на разработчиков). Попытка объяснить проблему в сжатой форме сама по себе была непростой задачей. Я также не уверен, что понимаю, чего не хватало изначально, но надеюсь, что исправленная версия поможет.

takeshi2010 25.06.2024 13:15
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
7
89
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я сам с этим сталкивался раньше, давайте изложим факты:

  • Событие является общим — имеет смысл, это какой-то класс, который имеет общие свойства, которые могут быть перезаписаны пользователями библиотеки.
  • Нам нужна функция, которая использует 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()))

... проверка типов у меня отлично работает как в строгом режиме, так и в строгом режиме mypy. Введя новый тип var, мы можем сделать один ковариантным, а другой — нековариантным. Надеюсь это поможет!

Извиняюсь, я только что заметил, что вопрос обновился, пока я это писал, но думаю, что все это по-прежнему применимо.

Mark 25.06.2024 13:11

Спасибо !!!! Я даже не знал, что смогу это сделать (переопределить новую TypeVar для другого использования).

takeshi2010 25.06.2024 13:20

Другие вопросы по теме