Ваш первый пример, к сожалению, небезопасен на законных основаниях - он нарушает так называемый «принцип подстановки Лискова».
Чтобы продемонстрировать, почему это так, позвольте мне немного упростить ваш пример: я сделаю так, чтобы базовый класс принимал любой вид 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 ++ ... Единственный контрпример, который я знает Эйфель.)
Мы потенциально можем столкнуться с аналогичными проблемами с вашим исходным примером. Чтобы сделать это немного более очевидным, позвольте мне переименовать некоторые из ваших классов, чтобы они были немного более реалистичными. Предположим, мы пытаемся написать какой-то механизм выполнения 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 имеет особое значение: он представляет собой полностью динамический тип. Когда вы говорите «переменная X имеет тип Any», вы на самом деле говорите: «Я не хочу, чтобы вы предполагали что-либо об этой переменной, но я хочу иметь возможность использовать этот тип. Я хочу, чтобы ты не жаловался! "
На самом деле, называть Any «самым широким типом» неточно. На самом деле это одновременно и самый широкий, и самый узкий тип. Каждый отдельный тип является подтипом Any AND Any является подтипом всех других типов. Mypy всегда выбирает ту позицию, которая не приводит к ошибкам проверки типов.
По сути, это аварийный выход, способ сказать контролеру типов: «Я знаю лучше». Каждый раз, когда вы задаете тип переменной Any, вы фактически полностью отказываетесь от проверки типов этой переменной, к лучшему или к худшему.
Подробнее об этом см. типизация. Любой vs объект?.
Наконец, что вы можете со всем этим поделать?
Что ж, к сожалению, я не уверен, что есть простой способ обойти это: вам придется переделывать свой код. Это в корне несостоятельно, и на самом деле нет никаких уловок, которые гарантированно вытащили бы вас из этого.
То, как именно вы это сделаете, зависит от того, что именно вы пытаетесь сделать. Возможно, вы могли бы что-то сделать с дженериками, как предложил один пользователь. Или, возможно, вы могли бы просто переименовать один из методов в соответствии с предложением другого. Или же вы можете изменить Base.fun так, чтобы он использовал тот же тип, что и Derived.fun, или наоборот; вы можете сделать так, чтобы Derived больше не наследовал от Base. Все действительно зависит от деталей ваших конкретных обстоятельств.
И, конечно, если ситуация действительно неразрешима, вы можете полностью отказаться от проверки типов в этом углу этой кодовой базы и заставить Base.fun (...) принять Any (и согласиться с тем, что вы может начать работать с ошибками во время выполнения).
Необходимость обдумать эти вопросы и изменить свой код может показаться неудобной проблемой - однако я лично считаю, что это стоит отметить! Mypy успешно предотвратил случайное внесение ошибки в ваш код и подталкивает вас к написанию более надежного кода.
person
Michael0x2a
schedule
25.01.2019