Почему mypy игнорирует переменную универсального типа, которая содержит тип, несовместимый с TypeVar?

#python #types #type-hinting #mypy

#python #типы #подсказка типа #mypy

Вопрос:

Ниже я определяю переменную типа, псевдоним универсального типа и функцию точечного произведения. mypy не выдает ошибку. Почему нет?

Я ожидал бы, что это вызовет ошибку для, v3 потому что это вектор строк, и я указал, что T должно быть int , float или complex .

 from typing import Any, Iterable, Tuple, TypeVar

T = TypeVar('T', int, float, complex)
Vector = Iterable[T]

def dot_product(a: Vector[T], b: Vector[T]) -> T:
    return sum(x * y for x, y in zip(a, b))

v1: Vector[int] = []    # same as Iterable[int], OK
v2: Vector[float] = []  # same as Iterable[float], OK
v3: Vector[str] = []    # no error - why not?
  

Комментарии:

1. Подсказки типа — это просто подсказки, а не ограничения. Они не влияют на выполнение программы, есть для линтеров, которые могут вносить предложения.

2. Смотрите PEP 484

3. @ralf: Да, я знаю, что подсказки типа не влияют на время выполнения. Мой вопрос касается того факта, что mypy это не вызывает ошибку.

Ответ №1:

Я думаю, проблема здесь в том, что когда вы создаете псевдоним типа, вы на самом деле не создаете новый тип — вы просто присваиваете псевдоним или альтернативное написание существующему.

И если все, что вы делаете, это предоставляете альтернативное написание типу, это означает, что при этом должно быть невозможно добавить какое-либо дополнительное поведение. Именно это здесь и происходит: вы пытаетесь добавить дополнительную информацию (ваши три ограничения типа) к Iterable, а mypy их игнорирует. В нижней части документов mypy об псевдонимах универсального типа есть примечание, в котором говорится в основном об этом.mypy docs on generic type aliases — псевдонимы универсального типа, которые содержат тип, несовместимый с TypeVar.

Тот факт, что mypy просто молча использует ваш TypeVar без предупреждения о том, что его дополнительные ограничения игнорируются, на самом деле кажется ошибкой. В частности, это похоже на ошибку удобства использования: Mypy должен был выдать здесь предупреждение и запретить использование чего-либо, кроме неограниченных typevars, внутри вашего псевдонима типа.


Итак, что вы можете сделать, чтобы ввести свой код?

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

Это означает, что пользователь может создать Vector[str] (он же an Iterable[str] ), но на самом деле это не имеет большого значения: они получат ошибку типа в тот момент, когда попытаются фактически передать ее в любую функцию, подобную вашей dot_product функции, которая использует псевдонимы типов.

Вторым решением было бы создать пользовательский vector подкласс. Если вы сделаете это, вы создадите новый тип и поэтому сможете фактически добавлять новые ограничения — но вы больше не сможете передавать списки и тому подобное непосредственно в свои dot_product классы: вам нужно будет обернуть их в свой пользовательский векторный класс.

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

Третье и окончательное решение заключается в определении пользовательского «векторного» протокола. Это позволило бы нам избежать необходимости заключать наши списки в какой-либо пользовательский класс — и мы создаем новый тип, чтобы мы могли добавлять любые ограничения, которые мы хотим. Например:

 from typing import Iterable, TypeVar, Iterator, List
from typing_extensions import Protocol

T = TypeVar('T', int, float, complex)

# Note: "class Vector(Protocol[T])" here means the same thing as 
# "class Vector(Protocol, Generic[T])".
class Vector(Protocol[T]):
    # Any object that implements these three methods with a compatible signature
    # is considered to be compatible with "Vector".

    def __iter__(self) -> Iterator[T]: ...

    def __getitem__(self, idx: int) -> T: ...

    def __setitem__(self, idx: int, val: T) -> None: ...

def dot_product(a: Vector[T], b: Vector[T]) -> T:
    return sum(x * y for x, y in zip(a, b))

v1: Vector[int] = []    # OK: List[int] is compatible with Vector[int]
v2: Vector[float] = []  # OK: List[float] is compatible with Vector[int]
v3: Vector[str] = []    # Error: Value of type variable "T" of "Vector" cannot be "str"

dot_product(v3, v3)  # Error: Value of type variable "T" of "dot_product" cannot be "str"

nums: List[int] = [1, 2, 3]
dot_product(nums, nums)  # OK: List[int] is compatible with Vector[int]
  

Основным недостатком этого подхода является то, что вы не можете добавить в свой протокол какие-либо методы с реальной логикой, которые вы можете повторно использовать между чем угодно, что может считаться «Вектором». (Ну, вы вроде как можете, но не в том смысле, который будет полезен в вашем примере).