Как проверить, что конкретный метод соблюдает подсказки типов для абстрактного метода - PullRequest
1 голос
/ 04 ноября 2019

Это вопрос из двух частей, но вторая часть зависит от первой части.

В образовательных целях я пытаюсь реализовать абстрактный базовый класс и набор тестов для групп (понятие из абстрактной алгебры). Часть определения алгебраической группы эквивалентна ограничению типа, и я хочу реализовать это ограничение типа на ABC, и мне есть что жаловаться, если методы конкретных классов не соответствуют этому ограничению.

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

from __future__ import annotations
from abc import ABC, abstractmethod


class AbsGroup(ABC):
    @abstractmethod
    def op(self, other: AbsGroup) -> AbsGroup:   # <-- Line-of-interest #1
        pass


class Bool(AbsGroup):

    def __init__(self, val="False"):

        if val not in ["True", "False"]:
            raise ValueError("Invalid Bool value %s" % val)

        self.val = val

    def op(self, other):
        """Logical AND"""
        if self.val == "True" and other.val == "True":  # <-- Line-of-interest #2
            return Bool("True")
        return Bool("False")

    def __eq__(self, other):
        return self.val == other.val

    def __repr__(self):
        return self.val

Во-первых: линия интересов # 1 - это то, что выполняет работу с ограничениями типов, но текущая реализация неверна. Он только проверяет, что метод получает и возвращает экземпляр AbsGroup. Это может быть любой AbsGroup экземпляр. Я хочу, чтобы он проверил, что для конкретного класса, по которому он наследуется, он получает и возвращает экземпляр этого конкретного класса (поэтому в случае Bool он получает и возвращает экземпляр Bool). Смысл упражнения заключается в том, чтобы сделать это в одном месте, а не устанавливать его конкретно для каждого конкретного класса. Я полагаю, что это делается с помощью некоторых обобщений типа хинтинг, которые немного глубже, чем я должен вникнуть в отношении хинтинга типов. Как мне это сделать?

Во-вторых: как мне проверить , что конкретный метод соответствует подсказке абстрактного типа? Инспектор типов в моей IDE (PyCharm) жалуется на интересующую линию №2, потому что он ожидает, что other будет иметь тип AbsGroup, который не имеет атрибута val. Это ожидаемо и исчезло бы, если бы я мог найти решение первой проблемы, но моя IDE - это only вещь, которую я могу найти, которая замечает это несоответствие. mypy по умолчанию ничего не говорит, как и flake8 и pylint. Здорово, что PyCharm на высоте, но если бы я хотел включить это в рабочий процесс, какую команду мне нужно было бы выполнить, которая потерпела бы неудачу в случае, если мой конкретный метод не соответствует абстрактной сигнатуре?

1 Ответ

1 голос
/ 04 ноября 2019

Первый совет: если mypy недостаточно говорит вам, попробуйте mypy --strict.

. Вы правильно поняли, что аннотация типа для op в базовом классе недостаточно ограничительна и фактически будетнесовместим с дочерним классом.

Посмотрите на этот нерабочий пример.

from __future__ import annotations
from abc import ABC, abstractmethod


class AbsGroup(ABC):
    @abstractmethod
    def op(self, other: AbsGroup) -> AbsGroup:
        pass


class Bool(AbsGroup):
    def __init__(self, val: str = "False") -> None:
        self.val = val

    def op(self, other: Bool) -> Bool:
        ...

Я аннотировал op в Bool с правильным типомно теперь mypy жалуется:

file.py: 15: ошибка: Аргумент 1 из "op" несовместим с супертипом "AbsGroup";supertype определяет тип аргумента как «AbsGroup»

У вас есть два варианта: либо сделать базовую аннотацию еще менее строгой (Any), либо сделать ваш класс Generic one:

from __future__ import annotations
from abc import ABC, abstractmethod

from typing import TypeVar, Generic


T = TypeVar('T')


class AbsGroup(Generic[T], ABC):
    @abstractmethod
    def op(self, other: T) -> T:
        pass

# EDIT: ADDED QUOTES AROUND Bool
class Bool(AbsGroup['Bool']):
    def __init__(self, val: str = "False") -> None:
        self.val = val

    def op(self, other: Bool) -> Bool:
        ...

Это включает в себя несколько шагов:

  1. создание переменной типа T (похоже на переменные универсального типа в других языках)
  2. пусть базовый класс также наследуется отGeneric[T] делая его универсальным классом
  3. измените метод op, чтобы он брал и возвращал T
  4. , чтобы дочерний класс наследовал от AbsGroup[Bool] (в C ++ это известно как CRTP )

Это заставляет замолчать mypy --strict, и PyCharm правильно выводит тип возврата op.

Редактировать:

Предыдущее определение дочернего класса выглядело так: class Bool(AbsGroup[Bool]): ... без кавычек. Но это не работает и выдает NameError при создании класса:

NameError: имя 'Bool' не определено

Это ожидаемое поведение, как написанов PEP 563 .

[...] Однако в модуле ввода есть API-интерфейсы, которые используют другие синтаксические конструкции языка, и для них по-прежнему потребуется обходной путь прямые ссылки с строковыми литералами . Список включает в себя: [...]

  • базовые классы:

    class C(Tuple['<type>', '<type>']): ...

Так чтов этом случае все еще требуются кавычки, хотя мы использовали будущий импорт.

Просто примечание: почему вы используете строковые символы для логических значений? Уже есть два прекрасно работающих экземпляра с именами True и False. Это сделает ваш код намного проще. Например, проверка в конструкторе может быть упрощена до if type(val) is bool (я бы не стал использовать isinstance здесь, поскольку вы не хотите, чтобы val был пользовательским типом, возможно?).

...