Методы классов в общих протоколах с самотипами, ошибка проверки типа mypy - PullRequest
0 голосов
/ 03 ноября 2018

Немного предыстории, мне необходимо определить тип оболочки int, скажем MyInt (среди некоторых других классов), и другой универсальный тип Interval, который может принимать MyInt объекты, а также другие типы объекты. Так как типы, приемлемые Interval, не попадают в аккуратную иерархию, я подумал, что это будет идеальный вариант использования для экспериментального Protocol, который в моем случае потребует пару методов и пару @classmethod s. Все методы возвращают «тип», то есть MyInt.my_method возвращает MyInt. Вот MCVE:

from dataclasses import dataclass
from typing import Union, ClassVar, TypeVar, Generic, Type

from typing_extensions import Protocol


_P = TypeVar('_P', bound='PType')
class PType(Protocol):
    @classmethod
    def maximum_type_value(cls: Type[_P]) -> _P:
        ...
    @classmethod
    def minimum_type_value(cls: Type[_P]) -> _P:
        ...
    def predecessor(self: _P) -> _P:
        ...
    def successor(self: _P) -> _P:
        ...

@dataclass
class MyInteger:
    value: int
    _MAX: ClassVar[int] = 42
    _MIN: ClassVar[int] = -42
    def __post_init__(self) -> None:
        if not (self._MIN <= self.value <= self._MAX):
            msg = f"Integers must be in range [{self._MIN}, {self._MAX}]"
            raise ValueError(msg)
    @classmethod
    def maximum_type_value(cls) -> MyInteger:
        return MyInteger(cls._MAX)
    @classmethod
    def minimum_type_value(cls) -> MyInteger:
        return MyInteger(cls._MIN)
    def predecessor(self) -> MyInteger:
        return MyInteger(self.value - 1)
    def successor(self) -> MyInteger:
        return MyInteger(self.value + 1)


@dataclass
class Interval(Generic[_P]):
    low: _P
    high: _P

interval = Interval(MyInteger(1), MyInteger(2))
def foo(x: PType) -> PType:
    return x
foo(MyInteger(42))

Однако, Mypy жалуется:

(py37) Juans-MacBook-Pro: juan$ mypy mcve.py
mcve.py:46: error: Value of type variable "_P" of "Interval" cannot be "MyInteger"
mcve.py:49: error: Argument 1 to "foo" has incompatible type "MyInteger"; expected "PType"
mcve.py:49: note: Following member(s) of "MyInteger" have conflicts:
mcve.py:49: note:     Expected:
mcve.py:49: note:         def maximum_type_value(cls) -> <nothing>
mcve.py:49: note:     Got:
mcve.py:49: note:         def maximum_type_value(cls) -> MyInteger
mcve.py:49: note:     Expected:
mcve.py:49: note:         def minimum_type_value(cls) -> <nothing>
mcve.py:49: note:     Got:
mcve.py:49: note:         def minimum_type_value(cls) -> MyInteger

Что мне трудно понять. Почему тип возврата ожидает <nothing>? Я пытался просто не аннотировать cls в протоколе:

_P = TypeVar('_P', bound='PType')
class PType(Protocol):
    @classmethod
    def maximum_type_value(cls) -> _P:
        ...
    @classmethod
    def minimum_type_value(cls) -> _P:
        ...
    def predecessor(self: _P) -> _P:
        ...
    def successor(self: _P) -> _P:
        ...

Однако mypy выдает похожее сообщение об ошибке:

mcve.py:46: error: Value of type variable "_P" of "Interval" cannot be "MyInteger"
mcve.py:49: error: Argument 1 to "foo" has incompatible type "MyInteger"; expected "PType"
mcve.py:49: note: Following member(s) of "MyInteger" have conflicts:
mcve.py:49: note:     Expected:
mcve.py:49: note:         def [_P <: PType] maximum_type_value(cls) -> _P
mcve.py:49: note:     Got:
mcve.py:49: note:         def maximum_type_value(cls) -> MyInteger
mcve.py:49: note:     Expected:
mcve.py:49: note:         def [_P <: PType] minimum_type_value(cls) -> _P
mcve.py:49: note:     Got:
mcve.py:49: note:         def minimum_type_value(cls) -> MyInteger

Что для меня, имеет еще меньше смысла. Обратите внимание, если я сделаю эти методы экземпляра:

_P = TypeVar('_P', bound='PType')
class PType(Protocol):
    def maximum_type_value(self: _P) -> _P:
        ...
    def minimum_type_value(self: _P) -> _P:
        ...
    def predecessor(self: _P) -> _P:
        ...
    def successor(self: _P) -> _P:
        ...

@dataclass
class MyInteger:
    value: int
    _MAX: ClassVar[int] = 42
    _MIN: ClassVar[int] = -42
    def __post_init__(self) -> None:
        if not (self._MIN <= self.value <= self._MAX):
            msg = f"Integers must be in range [{self._MIN}, {self._MAX}]"
            raise ValueError(msg)
    def maximum_type_value(self) -> MyInteger:
        return MyInteger(self._MAX)
    def minimum_type_value(self) -> MyInteger:
        return MyInteger(self._MIN)
    def predecessor(self) -> MyInteger:
        return MyInteger(self.value - 1)
    def successor(self) -> MyInteger:
        return MyInteger(self.value + 1)

Тогда mypy совсем не жалуется:

Я прочитал о самоподтипах в протоколах в PEP 544 , где приведен следующий пример:

C = TypeVar('C', bound='Copyable')
class Copyable(Protocol):
    def copy(self: C) -> C:

class One:
    def copy(self) -> 'One':
        ...

T = TypeVar('T', bound='Other')
class Other:
    def copy(self: T) -> T:
        ...

c: Copyable
c = One()  # OK
c = Other()  # Also OK

Кроме того, в PEP484, относительно набора методов класса , мы видим этот пример:

T = TypeVar('T', bound='C')
class C:
    @classmethod
    def factory(cls: Type[T]) -> T:
        # make a new instance of cls

class D(C): ...
d = D.factory()  # type here should be D

Что не так с моим Protocol / определением класса? Я что-то упускаю из виду? Я был бы признателен за любые конкретные ответы о , почему это не удается , или любой обходной путь. Но обратите внимание, мне нужно, чтобы эти атрибуты были доступны в классе.

Обратите внимание, я пытался использовать ClassVar, но это привело к другим проблемам ... а именно, ClassVar не принимает переменные типа , насколько я могу судить, ClassVar не может быть родовое . И в идеале это будет @classmethod, поскольку мне, возможно, придется полагаться на другие метаданные, которые я хотел бы использовать в классе.

1 Ответ

0 голосов
/ 07 ноября 2018

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

https://github.com/python/mypy/issues/3645

Проблема заключается в обработке переменных TypeVar в методах класса, а не в том, что напрямую связано с протоколами.

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

T = TypeVar('T')

class Factory(Generic[T]):
    def produce(self) -> T:
        ...
    @classmethod
    def get(cls) -> T:
        return cls().produce()

class HelloWorldFactory(Factory[str]):
    def produce(self) -> str:
        return 'Hello World'

reveal_type(HelloWorldFactory.get())  # mypy should be able to infer 'str' here

Выходные данные отive_type - это T, а не str. То же самое происходит с вашим кодом, когда Mypy не может определить тип должен быть MyInteger, а не _P, и поэтому ваш класс не видит реализацию протокола. Изменение типа возвращаемого значения методов класса на 'PType' устраняет ошибки, но я недостаточно уверен, чтобы знать, есть ли другие последствия этого изменения.

Были некоторые дискуссии о том, как лучше всего справиться с этим, потому что нетривиально определить, каким должно быть правильное поведение в каждом конкретном случае, поэтому может быть без вреда пометить это им для большего количества примеров использования (см. https://github.com/python/mypy/issues/5664 например.)

...