Определение, почему конкретный объект не реализует протокол - PullRequest
4 голосов
/ 04 марта 2020

Считайте, что я определяю протокол Frobbable. Кроме того, у меня есть правильная реализация протокола и сломанная реализация, в которой отсутствует метод .frob():

from typing import Protocol
from abc import abstractmethod


class Frobbable(Protocol):
    @abstractmethod
    def frob(self) -> None:
        raise NotImplementedError


def main(knob: Frobbable) -> None:
    knob.frob()


class Knob:
    def frob(self) -> None:
        print("knob has been frobbed")


class BrokenKnob:
    pass


main(Knob())
main(BrokenKnob())

Проверка этой программы с помощью mypy приводит к ошибке, как и ожидалось:

testprotocol.py:25:6: error: Argument 1 to "main" has incompatible type "BrokenKnob"; expected "Frobbable"  [arg-type]
    main(BrokenKnob())
         ^
Found 1 error in 1 file (checked 1 source file)

К сожалению, он не предоставляет никакой информации о , почему BrokenKnob несовместим: в этом случае отсутствует .frob(). Без такой информации исправление такой проблемы в нетривиальной программе (с протоколами со многими методами и многими реализациями) становится чрезвычайно утомительной работой.

Есть ли способ получить эту информацию из mypy или любого другого инструмента без модификации программы? Я знаю, что могу явно подкласс Frobbable, но это скорее отрицает цель использования Protocol.

1 Ответ

3 голосов
/ 04 марта 2020

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

Например, если мы настроим ваш пример так, чтобы он соответствовал этим ограничениям ...

from typing import Protocol
from abc import abstractmethod


class Frobbable(Protocol):
    @abstractmethod
    def frob(self) -> None:
        raise NotImplementedError
    @abstractmethod
    def bob(self) -> None:
        raise NotImplementedError


def main(knob: Frobbable) -> None:
    knob.frob()


class BrokenKnob:
    def frob(self) -> None:
        raise NotImplementedError


main(BrokenKnob())

... мы получим более описательное сообщение об ошибке, как и ожидалось:

test.py:23: error: Argument 1 to "main" has incompatible type "BrokenKnob"; expected "Frobbable"
test.py:23: note: 'BrokenKnob' is missing following 'Frobbable' protocol member:
test.py:23: note:     bob

Хотя эти эвристики кажутся разумными, я также думаю, что они могли бы, возможно, сделать с некоторыми дополнительными усовершенствованиями, чтобы лучше обрабатывать сценарии использования, такие как тот, с которым вы сталкиваетесь. Например, если все члены отсутствуют, я думаю, что для mypy было бы разумным сообщить об ошибке «этот объект ничего не реализует в протоколе» вместо более обобщенного c и, возможно, обработать случаи, когда слишком много пропавших без вести более изящно. Возможно, вы могли бы попытаться представить PR для улучшения этой эвристики, если вы готовы к этому?

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

  1. Убедитесь, что все члены Frobbable являются абстрактными (что вы уже делаете)
  2. Make BrokenKnob временно подкласс Frobbable
  3. Попробуйте временно создать новый экземпляр BrokenKnob.

Получившаяся ошибка, кажется, перечисляет все отсутствующие атрибуты без фиксированного ограничения.

error: Cannot instantiate abstract class 'BrokenKnob' with abstract attribute 'frob'

Затем, как только вы закончите sh, внося исправления, Вы можете отменить ваши временные изменения.

...