mypy: аргумент метода несовместим с супертипом - PullRequest
0 голосов
/ 24 января 2019

Посмотрите на пример кода (mypy_test.py):

import typing

class Base:
   def fun(self, a: str):
       pass

SomeType = typing.NewType('SomeType', str)

class Derived(Base):
    def fun(self, a: SomeType):
        pass

теперь mypy жалуется:

mypy mypy_test.py  
mypy_test.py:10: error: Argument 1 of "fun" incompatible with supertype "Base"

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

Версии программного обеспечения:

Mypy 0,650 Python 3.7.1

Что я пробовал:

import typing

class Base:
   def fun(self, a: typing.Type[str]):
       pass

SomeType = typing.NewType('SomeType', str)

class Derived(Base):
    def fun(self, a: SomeType):
        pass

Но это не помогло.

Один пользователь прокомментировал: «Похоже, вы не можете сузить допустимые типы в переопределенном методе?»

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

import typing

class Base:
   def fun(self, a: typing.Any):
       pass

SomeType = typing.NewType('SomeType', str)

class Derived(Base):
   def fun(self, a: SomeType):
       pass

Нет жалоб от mypy с кодом, приведенным выше.

Ответы [ 3 ]

0 голосов
/ 24 января 2019

Обе функции имеют одинаковое имя, поэтому просто переименуйте 1 из функций. mypy выдаст вам ту же ошибку, если вы сделаете это:

class Derived(Base):
        def fun(self, a: int):

Переименование fun в fun1 решает проблему с mypy, хотя это просто обходной путь для mypy.

class Base:
   def fun1(self, a: str):
       pass
0 голосов
/ 25 января 2019

Ваш первый пример, к сожалению, законно небезопасен - он нарушает что-то, известное как «принцип подстановки Лискова».

Чтобы продемонстрировать почему это так, позвольте мне упростить ваш пример:Немного: я сделаю так, чтобы базовый класс принял любой тип object, а дочерний класс принял int.Я также добавил немного логики времени выполнения: базовый класс просто выводит аргумент;класс Derived добавляет аргумент против некоторого произвольного типа int.

class Base:
    def fun(self, a: object) -> None:
        print("Inside Base", a)

class Derived(Base):
    def fun(self, a: int) -> None:
        print("Inside Derived", a + 10)

На первый взгляд, это выглядит прекрасно.Что может пойти не так?

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

def accepts_base(b: Base) -> None:
    b.fun("hello!")

accepts_base(Base())
accepts_base(Derived())

Вы можете увидеть, куда это идет - эта программа на самом деле небезопасна ипроизойдет сбой во время выполнения!В частности, самая последняя строка прерывается: мы передаем экземпляр Derived, а метод Derived fun принимает только целые числа.Затем он попытается сложить вместе полученную строку с 10 и быстро завершится с ошибкой TypeError.

Вот почему mypy запрещает вам сужать типы аргументов в перезаписываемом методе.Если Derived является подклассом Base, это означает, что мы должны иметь возможность заменить экземпляром Derived в любом месте, где мы используем Base, не нарушая ничего.Это правило особенно известно как принцип подстановки Лискова.

Сужение типов аргументов предотвращает это.

(Как примечание, тот факт, что mypy требует от вас уважения к Лискову, на самом деле довольно стандартенПрактически все статически типизированные языки с подтипами выполняют одно и то же - Java, C #, C ++ ... Единственный встречный пример, о котором я знаю, это Eiffel.)


Мы можем потенциальностолкнуться с аналогичными проблемами с вашим оригинальным примером.Чтобы сделать это немного более очевидным, позвольте мне переименовать некоторые из ваших классов в более реалистичные.Предположим, мы пытаемся написать какой-то механизм выполнения SQL и написать что-то похожее на это:

from typing import NewType

class BaseSQLExecutor:
    def execute(self, query: str) -> None: ...

SanitizedSQLQuery = NewType('SanitizedSQLQuery', str)

class PostgresSQLExecutor:
    def execute(self, query: SanitizedSQLQuery) -> None: ...

Обратите внимание, что этот код идентичен вашему первоначальному примеру!Единственное, что отличается, это имена.

Мы снова можем столкнуться с похожими проблемами во время выполнения - предположим, что мы использовали вышеупомянутые классы, например, так:

def run_query(executor: BaseSQLExecutor, query: str) -> None:
    executor.execute(query)

run_query(PostgresSQLExecutor, "my nasty unescaped and dangerous string")

Если это было разрешено проверять,мы ввели потенциальную уязвимость в наш код!Инвариант, что PostgresSQLExecutor может принимать только те строки, которые мы явно решили пометить как тип «SanitizedSQLQuery», не работает.


Теперь, чтобы ответить на другой вопрос: почему Mypy перестает жаловаться, есливместо этого мы заставляем Base принимать аргумент типа Any?

Ну, это потому, что тип Any имеет совершенно особый смысл: он представляет 100% полностью динамический тип.Когда вы говорите «переменная X имеет тип Any», вы фактически говорите: «Я не хочу, чтобы вы предполагали что-либо об этой переменной - и я хочу иметь возможность использовать этот тип, однако яхочу, чтобы вы не жаловались! "

На самом деле неточно называть Any" самым широким из возможных типов ".В действительности это одновременно и самый широкий тип, и самый узкий из возможных.Каждый отдельный тип является подтипом Any и Any является подтипом всех других типов.Mypy всегда выбирает любую позицию, которая приводит к отсутствию ошибок при проверке типов.

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

Подробнее об этом см. typing.Any vs object? .


Наконец, что вы можете сделать со всем этим?

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

Как именно вы будете это делать, зависит от того, что именно вы пытаетесь сделать.Возможно, вы могли бы сделать что-то с дженериками, как предложил один пользователь.Или, возможно, вы могли бы просто переименовать один из методов, как предложил другой.Или же вы можете изменить Base.fun так, чтобы он использовал тот же тип, что и Derived.fun, или наоборот;вы могли бы заставить Derived больше не наследовать от Base.Все это действительно зависит от деталей вашего точного обстоятельства.

И, конечно, если ситуация действительно является неразрешимой, вы можете полностью отказаться от проверки типов в этом углу этой кодовой базы.и заставьте Base.fun (...) принять любое (и согласиться с тем, что вы можете столкнуться с ошибками во время выполнения).

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

0 голосов
/ 24 января 2019

Используйте универсальный класс следующим образом:

from typing import Generic
from typing import NewType
from typing import TypeVar


BoundedStr = TypeVar('BoundedStr', bound=str)


class Base(Generic[BoundedStr]):
    def fun(self, a: BoundedStr) -> None:
        pass


SomeType = NewType('SomeType', str)


class Derived(Base[SomeType]):
    def fun(self, a: SomeType) -> None:
        pass

Идея состоит в том, чтобы определить базовый класс с универсальным типом.Теперь вы хотите, чтобы этот универсальный тип был подтипом str, следовательно, директива bound=str.

Затем вы определяете свой тип SomeType, а когда вы создаете подкласс Base, вы указываете, что такое универсальный.Переменная типа: в данном случае это SomeType.Затем mypy проверяет, что SomeType является подтипом str (поскольку мы заявили, что BoundedStr должно быть ограничено str), и в этом случае mypy счастлив.

Конечно, mypyбудет жаловаться, если вы определили SomeType = NewType('SomeType', int) и использовали его в качестве переменной типа для Base или, в более общем случае, если вы подкласс Base[SomeTypeVariable], если SomeTypeVariable не является подтипом str.

IПрочитайте в комментарии, что вы хотите угробить Mypy.Не надо!Вместо этого узнайте, как работают типы;когда вы чувствуете, что mypy против вас, очень вероятно, что вы что-то не совсем поняли.В этом случае обратитесь за помощью к другим людям, а не сдавайтесь!

...