Подсказка типа переопределения Python при возврате метода в дочернем классе без переопределения подписи метода

У меня есть базовый класс с подсказкой типа float при возврате метода.

В дочернем классе, не переопределяя сигнатуру, могу ли я как-то обновить подсказку типа при возврате метода, чтобы она была int?


Пример кода

#!/usr/bin/env python3.6


class SomeClass:
    """This class's some_method will return float."""

    RET_TYPE = float

    def some_method(self, some_input: str) -> float:
        return self.RET_TYPE(some_input)


class SomeChildClass(SomeClass):
    """This class's some_method will return int."""

    RET_TYPE = int


if __name__ == "__main__":
    ret: int = SomeChildClass().some_method("42"). # 
    ret2: float = SomeChildClass().some_method("42")

Моя IDE жалуется на несоответствие типов:

ожидаемый тип pycharm с плавающей запятой

Это происходит потому, что моя IDE все еще использует подсказку типа из SomeClass.some_method.


Исследования

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

Python: как переопределить тип намек на атрибут экземпляра в подклассе?

Предлагает использовать аннотации переменных экземпляра, но я не уверен, как это сделать для возвращаемого типа.


person Intrastellar Explorer    schedule 10.04.2020    source источник
comment
Я не думаю, что это будет возможно, потому что в вашем случае есть только один some_method (тот, который определен на SomeClass). Экземпляры SomeChildClass будут разрешать имя метода в тот же объект функции, определенный в SomeClass, на самом деле у них нет собственной отдельной реализации, которую вы могли бы аннотировать отдельно. Аннотации — это просто атрибут объекта функции, он не может иметь разные аннотации в зависимости от того, как он был разрешен с помощью MRO.   -  person wim    schedule 13.04.2020
comment
Ааа, это имеет смысл @wim, спасибо за ответ. Есть ли у вас какие-либо идеи о самом простом способе обновления аннотации в дочернем классе (SomeChildClass)?   -  person Intrastellar Explorer    schedule 13.04.2020
comment
Хм, самый простой способ: вместо того, чтобы делать это с атрибутом класса RET_TYPE = float, просто переопределить метод в дочернем классе?   -  person wim    schedule 13.04.2020
comment
Чтобы добавить к разговору: если вы измените строку на def some_method(self, some_input: str) -> RET_TYPE: (поэтому замените подсказку типа 'float' на подсказку 'RET_TYPE', тогда индикация ошибки в представлении исчезнет ( к сожалению, не в main), по крайней мере, в моем PyCharm. Очевидно, PyCharm достаточно умен, чтобы обнаружить, что RET_TYPE отличается для каждого вызова.   -  person Paul Rene    schedule 13.04.2020


Ответы (3)


Следующий код прекрасно работает на PyCharm. Я добавил регистр complex, чтобы было понятнее.

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

from typing import ClassVar, Generic, TypeVar, Callable


S = TypeVar('S', bound=complex)


class SomeMethodImplementor(Generic[S]):
    RET_TYPE: ClassVar[Callable]

    def some_method(self, some_input: str) -> S:
        return self.__class__.RET_TYPE(some_input)


class SomeClass(SomeMethodImplementor[complex]):
    RET_TYPE = complex


class SomeChildClass(SomeClass, SomeMethodImplementor[float]):
    RET_TYPE = float


class OtherChildClass(SomeChildClass, SomeMethodImplementor[int]):
    RET_TYPE = int


if __name__ == "__main__":
    ret: complex = SomeClass().some_method("42")
    ret2: float = SomeChildClass().some_method("42")
    ret3: int = OtherChildClass().some_method("42")
    print(ret, type(ret), ret2, type(ret2), ret3, type(ret3))

Если вы измените, например, ret2: float на ret2: int, он будет правильно показывать ошибку типа.

К сожалению, mypy действительно показывает ошибки в этом случае (версия 0.770),

otherhint.py:20: error: Incompatible types in assignment (expression has type "Type[float]", base class "SomeClass" defined the type as "Type[complex]")
otherhint.py:24: error: Incompatible types in assignment (expression has type "Type[int]", base class "SomeClass" defined the type as "Type[complex]")
otherhint.py:29: error: Incompatible types in assignment (expression has type "complex", variable has type "float")
otherhint.py:30: error: Incompatible types in assignment (expression has type "complex", variable has type "int")

Первые ошибки можно «исправить», написав

    RET_TYPE: ClassVar[Callable] = int

для каждого подкласса. Теперь ошибки уменьшаются до

otherhint.py:29: error: Incompatible types in assignment (expression has type "complex", variable has type "float")
otherhint.py:30: error: Incompatible types in assignment (expression has type "complex", variable has type "int")

которые прямо противоположны тому, что мы хотим, но если вы заботитесь только о PyCharm, это не имеет большого значения.

person Kevin Languasco    schedule 14.04.2020
comment
Спасибо @KevinLanguasco за ответ! Я не лучший программист, не могли бы вы объяснить, почему вам нужно было использовать bound=complex с S = TypeVar('S', bound=complex)? - person Intrastellar Explorer; 15.04.2020
comment
О, это на самом деле не нужно! Это просто говорит о том, что каждый конкретный тип для замены S должен быть подтипом комплекса. Поэтому, если вы напишете, скажем, SomeMethodImplementor[str], будет отправлено предупреждение, поскольку str не является подтипом complex. Просто дополнительная безопасность типов :) python .org/dev/peps/pep-0484/ (я использую тот факт, что int является подтипом float, который является подтипом complex) - person Kevin Languasco; 15.04.2020
comment
Хорошо, @KevinLanguasco, я присуждаю тебе награду, потому что твой ответ заполнил много пробелов, которые я пропустил. Мне удалось найти гибридное решение (которое я опубликовал), которое также не генерирует ошибок в mypy, если вам интересно/есть какие-либо мысли - person Intrastellar Explorer; 16.04.2020

Вы можете просто использовать что-то вроде этого:

from typing import TypeVar, Generic


T = TypeVar('T', float, int) # types you support


class SomeClass(Generic[T]):
    """This class's some_method will return float."""

    RET_TYPE = float

    def some_method(self, some_input: str) -> T:
        return self.RET_TYPE(some_input)


class SomeChildClass(SomeClass[int]):
    """This class's some_method will return int."""

    RET_TYPE = int


if __name__ == "__main__":
    ret: int = SomeChildClass().some_method("42")
    ret2: float = SomeChildClass().some_method("42")

Но есть одна проблема. Что не знаю как решить. Для метода SomeChildClass some_method IDE покажет общую подсказку. По крайней мере, pycharm (я полагаю, вы это делаете) не показывает это как ошибку.

person Anton Pomieshchenko    schedule 14.04.2020
comment
Спасибо @AntonPomeeshchenko за ответ! - person Intrastellar Explorer; 16.04.2020

Итак, я смог поиграть и объединить ответы от @AntonPomeeshcheko и @KevinLanguasco, чтобы найти решение, в котором:

  • Моя IDE (PyCharm) может правильно определить возвращаемый тип
  • mypy сообщает о несоответствии типов
  • Не вызывает ошибок во время выполнения, даже если подсказки типов указывают на несоответствие

Это именно то поведение, которое я хотел. Большое спасибо всем :)

#!/usr/bin/env python3

from typing import TypeVar, Generic, ClassVar, Callable


T = TypeVar("T", float, int)  # types supported


class SomeBaseClass(Generic[T]):
    """This base class's some_method will return a supported type."""

    RET_TYPE: ClassVar[Callable]

    def some_method(self, some_input: str) -> T:
        return self.RET_TYPE(some_input)


class SomeChildClass1(SomeBaseClass[float]):
    """This child class's some_method will return a float."""

    RET_TYPE = float


class SomeChildClass2(SomeBaseClass[int]):
    """This child class's some_method will return an int."""

    RET_TYPE = int


class SomeChildClass3(SomeBaseClass[complex]):
    """This child class's some_method will return a complex."""

    RET_TYPE = complex


if __name__ == "__main__":
    some_class_1_ret: float = SomeChildClass1().some_method("42")
    some_class_2_ret: int = SomeChildClass2().some_method("42")

    # PyCharm can infer this return is a complex.  However, running mypy on
    # this will report (this is desirable to me):
    # error: Value of type variable "T" of "SomeBaseClass" cannot be "complex"
    some_class_3_ret = SomeChildClass3().some_method("42")

    print(
        f"some_class_1_ret = {some_class_1_ret} of type {type(some_class_1_ret)}\n"
        f"some_class_2_ret = {some_class_2_ret} of type {type(some_class_2_ret)}\n"
        f"some_class_3_ret = {some_class_3_ret} of type {type(some_class_3_ret)}\n"
    )
person Intrastellar Explorer    schedule 15.04.2020
comment
Это работает, если это соответствует вашему варианту использования, но обратите внимание, что если вы хотите иметь иерархию подклассов (как предлагается в вопросе), например class SomeChildClass2(SomeChildClass1):, вы в конечном итоге окажетесь там, где вы начали в основном. - person Kevin Languasco; 16.04.2020