Как аннотировать атрибут, который может быть реализован как свойство?

Я пытаюсь порадовать mypy аннотациями моего типа. Вот минимальный пример:

class FooInterface:
    x: int


class FooWithAttribute(FooInterface):
    x: int = 0


class FooWithProperty(FooInterface):
    @property
    def x(self) -> int:
        return 0

Насколько я понимаю, все в порядке: и FooWithAttribute().x, и FooWithProperty().x вернут 0, что равно int, без ошибок типа. Однако mypy жалуется:

error: Signature of "x" incompatible with supertype "FooInterface"

Есть ли способ сказать mypy, что все в порядке? Прямо сейчас единственный способ, который я нашел, - это аннотировать x: typing.Any в FooInterface, что тратит впустую информацию о том, что x является int.


person sanyassh    schedule 11.10.2019    source источник


Ответы (1)


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

def mutate(f: FooInterface) -> None:
    f.x = 100

Вроде нормально, правда? Но что будет, если мы сделаем mutate(FooWithProperty())? Python действительно вылетит из-за ошибки AttributeError!

Traceback (most recent call last):
  File "test.py", line 19, in <module>
    mutate(FooWithProperty())
  File "test.py", line 16, in mutate
    f.x = 100
AttributeError: can't set attribute

Чтобы осчастливить mypy, у вас есть два варианта:

  1. Сделать FooInterface.x доступным только для чтения
  2. Реализуйте сеттер для FooWithProperty.x, чтобы сделать его доступным для записи

Я предполагаю, что в вашем случае вы, вероятно, захотите использовать подход 1. Если вы это сделаете, mypy правильно укажет, что строка f.x = 100 не разрешена:

from abc import abstractmethod

class FooInterface:
    # Marking this property as abstract is *optional*. If you do it,
    # mypy will complain if you forget to define x in a subclass.
    @property
    @abstractmethod
    def x(self) -> int: ...

class FooWithAttribute(FooInterface):
    # No complaints from mypy here: having this attribute be writable
    # won't violate the Liskov substitution principle -- it's safe to
    # use FooWithAttribute in any location that expects a FooInterface.
    x: int = 0

class FooWithProperty(FooInterface):
    @property
    def x(self) -> int:
        return 0

def mutate(f: FooInterface) -> None:
    # error: Property "x" defined in "FooInterface" is read-only
    f.x = 100

mutate(FooWithProperty())

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

person Michael0x2a    schedule 11.10.2019
comment
Отличный ответ, спасибо! Конечно, пришлось реализовать сеттер, просто забыл это сделать. Итак, я закончил с абстрактным свойством x с геттером и сеттером в FooInterface. Это позволяет обоим подклассам иметь собственную реализацию, с атрибутом или со свойством. И mypy доволен :) - person sanyassh; 12.10.2019