Переменная типа Dict для неуниверсальных методов класса

В следующем коде:

from typing import Generic
from typing import TypeVar
from typing import reveal_type


T = TypeVar('T')


class Field(Generic[T]):
    """A field definition with a default value."""

    def __init__(self, default_value: T):
        self.default_value = default_value


class FieldDataCollection:
    """A collection of field values."""

    def __init__(self, value_per_field: dict[Field[T], T]) -> None:
        self._value_per_field = value_per_field

    def get_field_value(self, field: Field[T]) -> T:
        """Return the field value if in the collection or the field default value."""
        return self._value_per_field.get(field, field.default_value)


if __name__ == '__main__':
    foo = Field(1)
    value = FieldDataCollection({foo: 2}).get_field_value(foo)
    reveal_type(value)

Я получаю следующие ошибки от mypy с версией 1.11.0 (но не с предыдущими версиями), работающей в строгом режиме:

error: Incompatible return value type (got "T@__init__", expected "T@get_field_value")  [return-value]
error: Argument 1 to "get" of "dict" has incompatible type "Field[T@get_field_value]"; expected "Field[T@__init__]"  [arg-type]
error: Argument 2 to "get" of "dict" has incompatible type "T@get_field_value"; expected "T@__init__"  [arg-type]
note: Revealed type is "builtins.int"

Насколько я понимаю об этих ошибках, mypy жалуется, что тип, выведенный в методе __init__, каким-то образом отличается от типа в методе get_field_value.

Я попытался использовать переменную другого типа для get_field_value:

T = TypeVar("T")
Tfield = TypeVar("Tfield")

...

class FieldDataCollection:
    ...

    def get_field_value(self, field: Field[Tfield]) -> Tfield:
        ...

Но mypy, кажется, уловил разницу и жалуется точно так же:

error: Incompatible return value type (got "T", expected "Tfield")  [return-value]
error: Argument 1 to "get" of "dict" has incompatible type "Field[Tfield]"; expected "Field[T]"  [arg-type]
error: Argument 2 to "get" of "dict" has incompatible type "Tfield"; expected "T"  [arg-type]
note: Revealed type is "builtins.int"

Я попробовал использовать Mapping или MutableMapping вместо dict, это работает нормально, но тогда я не могу использовать методы dict, такие как copy.

Вы думаете, что это регресс с моей стороны? В противном случае, есть ли у вас идеи, как правильно аннотировать класс FieldDataCollection?

Я думаю, вам также следует сделать FieldDataCollection универсальным. В настоящее время класс не является универсальным, а является универсальным только метод. Первая ошибка жалуется на то, что T из init не обязательно является тем же T, что и в вашем методе get, поскольку оба метода имеют независимый T, поскольку класс сам по себе не является универсальным в T.

luk2302 24.07.2024 23:50

@ luk2302 luk2302 Пожалуйста, опубликуйте это как ответ.

InSync 25.07.2024 06:37

@ luk2302 Спасибо за ответ. Сделать FieldDataCollection универсальным для T — это не то поведение, которое мне нужно, поскольку оно ограничит тип поля, которое я могу вставить в словарь. Потенциально можно вставить Field[str] или Field[int].

PoloB 25.07.2024 09:25

Вы хотите иметь возможность вставлять несколько разных вариантов полей? В этот момент вы больше не можете использовать какую-либо типобезопасность, и вам нужно по сути явно игнорировать ограничения mypy, потому что mypy прав: типы несовместимы, и нет способа сделать их совместимыми. Когда вы используете FieldDataCollection({foo: 2}), вы фактически создаете dict[Field[int], int], а затем пытаетесь получить к нему доступ с помощью (потенциально другого) Field[T]. Я не уверен в используемой здесь терминологии, но T в вашем dict может иметь только одно конкретное значение, оно не может быть одновременно str и int.

luk2302 25.07.2024 11:02

Что-то вроде {foo: 2, Field("x"): "y"} будет связывать T с object, наиболее конкретным общим суперклассом int и str. В этом случае ни одна из записей не будет вводить проверку, поскольку dict является инвариантом. (Изменение dict на typing.Mapping исправит это, но все равно оставит проблему слишком общего T.)

chepner 25.07.2024 15:26
Почему в 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 может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
6
90
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Кажется, на самом деле это проблема с областью действия: тип T в FieldDataCollection не определен, поэтому интерпретация другая, и в данном случае не та интерпретация, которую вы искали. Добавление Generic[T] к FieldDataCollection решает эту проблему для mypy версии 1.11.0, поскольку гарантирует, что тип, используемый в __init__, должен быть таким же, как тот, который используется в get_field_value.

Кроме того, как указано в комментариях, чтобы гарантировать возможность использования как Field[str], так и Field[int] в FieldDataCollection, мы должны сделать T ковариантным:

from typing import Generic
from typing import TypeVar
from typing import reveal_type


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


class Field(Generic[T]):
    """A field definition with a default value."""

    def __init__(self, default_value: T):
        self.default_value = default_value


class FieldDataCollection(Generic[T]):
    """A collection of field values."""

    def __init__(self, value_per_field: dict[Field[T], T]) -> None:
        self._value_per_field = value_per_field

    def get_field_value(self, field: Field[T]) -> T:
        """Return the field value if in the collection or the field default value."""
        return self._value_per_field.get(field, field.default_value)


if __name__ == '__main__':
    foo = Field(1)
    foo_str  = Field("test")
    value = FieldDataCollection({foo: 2, foo_str: "v"}).get_field_value(foo)
    reveal_type(value)

Надеюсь это поможет!

Обновление: просматривая предыдущие комментарии, я расширил его, чтобы обеспечить возможность использования нескольких типов ключей.

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

Mark 26.07.2024 14:39

Отлично! Большое спасибо за совет. Как бы вы справились с set_field_value(self, field: Field[T], value: T) -> None? Кажется, ковариантные переменные нельзя использовать в качестве параметра.

PoloB 27.07.2024 00:45

Как упоминалось в комментариях, в этом случае T становится объектом, потому что это общий супертип str и int. См. выходные данные раскрывающего_типа значения или саму коллекцию FieldDataCollection. Поэтому дженерик сейчас практически бесполезен.

luk2302 27.07.2024 14:39

@ luk2302 в Pyright, по крайней мере, показывает show_type int | str, интересно, может быть, в mypy это работает по-другому? @PoloB Я подумаю об изменении set_field_value

Mark 30.07.2024 11:58

@Марк, ну, очевидно, что это тоже правильный вывод типа. Но тот, который в равной степени бесполезен, потому что какой из них сейчас: int или str? value или объединение всех возможных типов означает, что вы не знаете тип.

luk2302 30.07.2024 12:39
int | str по-прежнему гораздо более конкретен, чем object — я бы сказал, что это лучшее, что вы можете определить, если только вы не используете TypedDict для представления объекта dict. dict в стандартной формулировке должен иметь один тип для представления ключей, а в приведенном выше примере тип ключа — Field[int | str].
Mark 30.07.2024 13:19

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