Встроенные аннотации типа по сравнению с заглушкой приводят к другому поведению mypy - PullRequest
1 голос
/ 20 марта 2020

Мой проект зависит от другого проекта, который хранит аннотации типов в файлах-заглушках. В файле .py другой проект определяет базовый класс, который мне нужно унаследовать от следующей

# within a .py file

class Foo:
    def bar(self, *baz):
        raise NotImplementedError

В соответствующей заглушке .pyi они аннотируют ее следующим образом:

# whitin a .pyi file

from typing import Generic, TypeVar, Callable

T_co = TypeVar("T_co", covariant=True)

class Foo(Generic[T_co]):
    bar: Callable[..., T_co]

Для моего проекта я хочу сделать аннотации типов встроенными, т.е. в файле .py, и попытался сделать это в подклассе Foo следующим образом:

# within a .py file

class SubFoo(Foo):
    def bar(self, baz: float) -> str:
        pass

Запуск mypy приводит к следующей ошибке

error: Signature of "bar" incompatible with supertype "Foo"

Если я удалю свои встроенные аннотации и добавлю это в заглушку .pyi

# within a .pyi file

class SubFoo(Foo):
    bar: Callable[[float], str]

mypy работает нормально.

Я думал, что оба метода эквивалентны, но, видимо, это не так. Может кто-нибудь объяснить мне, как они отличаются и что мне нужно изменить, чтобы сделать эту работу с встроенными аннотациями?


В комментариях @ Michael0x2a стало ясно, что ошибка воспроизводима только в том случае, если вы действительно используйте .py и .pyi файл. Вы можете скачать примеры выше здесь .

1 Ответ

1 голос
/ 21 марта 2020

В качестве предупреждения, мне неясно, как именно выглядит ваш код. У вас есть несколько разных версий Foo, и я не уверен, какую именно вы пытаетесь подклассить - в вашем вопросе отсутствует минимально воспроизводимый пример .

Но я предполагаю, что вы пытаетесь сделать что-то подобное?

class Foo:
    def bar(self, *baz: float) -> str:
        raise NotImplementedError

class SubFoo(Foo):
    def bar(self, baz: float) -> str:
        pass

Если это так, проблема заключается в том, что согласно сигнатуре базового класса это будет Разрешается делать что-то подобное, поскольку Foo.bar(...) определено для приема переменного числа аргументов.

f = Foo()
f.bar(1, 2, 3, 4, 5, 6, 7, 8)

Но если мы попытаемся использовать ваш подкласс вместо Foo, этот код потерпит неудачу, поскольку он принимает только один аргумент.

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

Но в таком случае зачем делать следующую проверку типа?

class Foo:
    bar: Callable[..., str]

class SubFoo(Foo):
    def bar(self, baz: float) -> str:
        pass

Это потому, что поскольку подпись родительского типа Callable[..., str], mypy фактически заканчивается пропуская проверку аргументов функции вообще. ... в основном говорит: «Пожалуйста, не беспокойтесь о проверке типов, связанной с моими аргументами».

Это похоже на то, как использование типа Any позволяет смешивать типы Dynami c со stati c из них. Точно так же Callable[..., str] позволяет вам express вызывать с динамическими / неопределенными сигнатурами.

Сравните это со следующей программой:

class Foo:
    def bar(self, *args: Any, **kwargs: Any) -> str:
        pass

class SubFoo(Foo):
    def bar(self, baz: float) -> str:
        pass

В отличие от предыдущей, эта программа делает не проверка типа - хотя Foo.bar все еще может принимать любые аргументы, «структура» аргументов в этом случае не остается гибкой, и mypy теперь будет настаивать на том, что ваш подкласс также должен быть способен принимать произвольное число Аргументы.


В качестве заключительного замечания важно отметить, что ни одно из этого поведения не имеет никакого отношения к тому, определены ли ваши подсказки типа в заглушке или нет. Скорее, все сводится к тому, каковы фактические типы ваших функций.

...