В следующем коде:
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?
@ luk2302 luk2302 Пожалуйста, опубликуйте это как ответ.
@ luk2302 Спасибо за ответ. Сделать FieldDataCollection универсальным для T — это не то поведение, которое мне нужно, поскольку оно ограничит тип поля, которое я могу вставить в словарь. Потенциально можно вставить Field[str] или Field[int].
Вы хотите иметь возможность вставлять несколько разных вариантов полей? В этот момент вы больше не можете использовать какую-либо типобезопасность, и вам нужно по сути явно игнорировать ограничения mypy, потому что mypy прав: типы несовместимы, и нет способа сделать их совместимыми. Когда вы используете FieldDataCollection({foo: 2}), вы фактически создаете dict[Field[int], int], а затем пытаетесь получить к нему доступ с помощью (потенциально другого) Field[T]. Я не уверен в используемой здесь терминологии, но T в вашем dict может иметь только одно конкретное значение, оно не может быть одновременно str и int.
Что-то вроде {foo: 2, Field("x"): "y"} будет связывать T с object, наиболее конкретным общим суперклассом int и str. В этом случае ни одна из записей не будет вводить проверку, поскольку dict является инвариантом. (Изменение dict на typing.Mapping исправит это, но все равно оставит проблему слишком общего T.)
Связанный материал: Как добавить подсказки типов для словаря, который сопоставляет класс с экземпляром этого класса?.






Кажется, на самом деле это проблема с областью действия: тип 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)
Надеюсь это поможет!
Обновление: просматривая предыдущие комментарии, я расширил его, чтобы обеспечить возможность использования нескольких типов ключей.
Ах, извините, я только что понял, что в разделе комментариев к предыдущему посту это обсуждается - при необходимости я исправлю.
Отлично! Большое спасибо за совет. Как бы вы справились с set_field_value(self, field: Field[T], value: T) -> None? Кажется, ковариантные переменные нельзя использовать в качестве параметра.
Как упоминалось в комментариях, в этом случае T становится объектом, потому что это общий супертип str и int. См. выходные данные раскрывающего_типа значения или саму коллекцию FieldDataCollection. Поэтому дженерик сейчас практически бесполезен.
@ luk2302 в Pyright, по крайней мере, показывает show_type int | str, интересно, может быть, в mypy это работает по-другому? @PoloB Я подумаю об изменении set_field_value
@Марк, ну, очевидно, что это тоже правильный вывод типа. Но тот, который в равной степени бесполезен, потому что какой из них сейчас: int или str? value или объединение всех возможных типов означает, что вы не знаете тип.
int | str по-прежнему гораздо более конкретен, чем object — я бы сказал, что это лучшее, что вы можете определить, если только вы не используете TypedDict для представления объекта dict. dict в стандартной формулировке должен иметь один тип для представления ключей, а в приведенном выше примере тип ключа — Field[int | str].
Я думаю, вам также следует сделать FieldDataCollection универсальным. В настоящее время класс не является универсальным, а является универсальным только метод. Первая ошибка жалуется на то, что T из init не обязательно является тем же T, что и в вашем методе get, поскольку оба метода имеют независимый T, поскольку класс сам по себе не является универсальным в T.