mypy: аргумент метода несовместим с супертипом

Посмотрите на пример кода (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 с приведенным выше кодом.


person Community    schedule 24.01.2019    source источник
comment
Похоже, вы не можете сузить список допустимых типов в переопределенном методе?   -  person maciek    schedule 24.01.2019


Ответы (3)


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

Чтобы продемонстрировать, почему это так, позвольте мне немного упростить ваш пример: я сделаю так, чтобы базовый класс принимал любой вид 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
comment
Боже, какой твердый ответ! - person Ojomio; 07.05.2020
comment
А как насчет производных типов вывода? Почему сужение типа вывода является проблемой или небезопасно? - person dba; 26.06.2020
comment
@dba - сужение производного типа вывода на самом деле безопасно! Полное правило состоит в том, что небезопасно сужать производные типы ввода и небезопасно расширять производные типы вывода. (Или, говоря более формальным языком, функции контравариантны по типу ввода и ковариантны по типу вывода.) Так, например, если родительский тип вывода - int, он Было бы небезопасно расширять производный выходной тип до object. В моем ответе обсуждались только правила для типов ввода, потому что именно об этом OP более или менее спрашивал в своем вопросе. - person Michael0x2a; 26.06.2020

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

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.

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

person gniourf_gniourf    schedule 24.01.2019

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

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

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

class Base:
   def fun1(self, a: str):
       pass
person Frans    schedule 24.01.2019