Абстрактное свойство Python () «Не может создать экземпляр абстрактного класса [] с помощью абстрактных методов», но я это сделал

#python #python-3.x #class #oop #abstract

#python #python-3.x #класс #ооп #аннотация

Вопрос:

Я пытаюсь создать базовый класс с рядом абстрактных свойств python на python 3.7.

Я попробовал это одним способом (см. «начать» ниже), используя аннотации @property, @abstractmethod, @property.setter . Это сработало, но не вызывает исключения, если подкласс не реализует сеттер. В этом смысл использования @abstract для меня, так что это бесполезно.

Поэтому я попытался сделать это другим способом (см. ‘end’ ниже), используя два метода @abstractmethod и ‘property()’, который сам по себе не является абстрактным, но использует эти методы. Этот подход генерирует ошибку при создании экземпляра подкласса:

         # {TypeError}Can't instantiate abstract class FirstStep with abstract methods end
 

Я четко реализую абстрактные методы, поэтому я не понимаю, что это значит. Свойство ‘end’ не помечено @abstract, но если я его прокомментирую, оно выполняется (но я не получаю свое свойство). Я также добавил этот тестовый неабстрактный метод ‘test_elapsed_time’, чтобы продемонстрировать, что у меня есть правильная структура класса и абстракция (это работает).

Есть ли вероятность, что я делаю что-то глупое, или есть какое-то особое поведение вокруг property(), которое вызывает это?

 class ParentTask(Task):
    def get_first_step(self):
        # {TypeError}Can't instantiate abstract class FirstStep with abstract methods end
        return FirstStep(self)


class Step(ABC):
    #     __metaclass__ = ABCMeta
    def __init__(self, task):
        self.task = task

    # First approach.  Works, but no warnings if don't implement setter in subclass
    @property
    @abstractmethod
    def start(self):
        pass

    @start.setter
    @abstractmethod
    def start(self, value):
        pass

    # Second approach.  "This method for 'end' may look slight messier, but raises errors if not implemented.
    @abstractmethod
    def get_end(self):
        pass

    @abstractmethod
    def set_end(self, value):
        pass

    end = property(get_end, set_end)

    def test_elapsed_time(self):
        return self.get_end() - self.start


class FirstStep(Step):
    @property
    def start(self):
        return self.task.start_dt

    # No warnings if this is commented out.
    @start.setter
    def start(self, value):
        self.task.start_dt = value

    def get_end(self):
        return self.task.end_dt

    def set_end(self, value):
        self.task.end_dt = value
 

Комментарии:

1. Я не могу воспроизвести вашу проблему с start помощью 3.7.4 или 3.8.5. Упрощенный пример с just start , no *end и пустым определением FirstStep raises , которое я получаю TypeError: Can't instantiate abstract class FirstStep with abstract methods start . Это работает с 3.3.

2. О, я понимаю. Вы определили средство получения, но не задающее средство, и вы хотите, чтобы требовались как средство получения, так и средство установки. Игнорируйте меня.

3. Да, ответ @chepner, похоже, объясняет эту часть. Я все еще пытаюсь переварить остальное.

Ответ №1:

Я подозреваю, что это ошибка во взаимодействии абстрактных методов и свойств.

В вашем базовом классе происходят следующие вещи, в порядке:

  1. Вы определяете абстрактный метод с именем start .
  2. Вы создаете новое свойство, которое использует абстрактный метод из 1) в качестве его получателя. Теперь имя start ссылается на это свойство, и единственная ссылка на исходное имя теперь хранится Self.start.fget .
  3. Python сохраняет временную ссылку на start.setter , потому что имя start будет привязано к еще одному объекту.
  4. Вы создаете второй абстрактный метод с именем start
  5. Ссылке из 3) присваивается абстрактный метод из 4) для определения нового свойства для замены однажды связанного с именем start . Это свойство имеет в качестве получателя метод из 1 и в качестве установщика метод из 4). Теперь start ссылается на это свойство; start.fget ссылается на метод из 1); start.fset ссылается на метод из 4).

На данный момент у вас есть свойство, компоненты которого являются абстрактными методами. Само свойство не было оформлено как абстрактное, но определение property.__isabstractmethod__ помечает его как таковое, потому что все его составляющие методы являются абстрактными. Что еще более важно, у вас есть следующие записи в Step.__abstractmethods__ :

  1. start , property
  2. end , property
  3. set_end , установщик для end
  4. gen_end , получатель для end

Обратите внимание, что функции компонента для start свойства отсутствуют, поскольку __abstractmethods__ хранятся имена, а не ссылки на вещи, которые необходимо переопределить. Использование property и метод результирующего свойства setter в качестве декораторов многократно заменяют то, на что start ссылается имя.

Теперь в вашем дочернем классе вы определяете новое свойство с именем start , затеняющее унаследованное свойство, у которого нет установщика и конкретного метода в качестве его получателя. На данный момент не имеет значения, предоставляете ли вы сеттер для этого свойства или нет, потому что, что касается abc оборудования, вы предоставили все, что он просил:

  1. Конкретный метод для имени start
  2. Конкретные методы для имен get_end и set_end
  3. Неявно конкретное определение для имени end , потому что всем базовым функциям для свойства end были предоставлены конкретные определения.

Комментарии:

1. В документах для abstractproperty (устаревших) документируется подход замены только @abstractmethod на @property . В приведенном примере прямо указано, что свойства могут быть частично абстрактными (например, только установщик, а не получатель в их примере) и что вам нужно перезаписать только эту часть property (подразумевая, что если у вас есть несколько абстрактных компонентов, вы должны перезаписать все абстрактные компоненты), тот факт, что на самом деле это не тактакое поведение определенно представляет собой ошибку.

2. По-видимому, для этого есть закрытая ошибка . Похоже, что смысл, который я там прочитал, на самом деле не существует; вы можете перезаписать отдельные элементы, ссылаясь на родительское свойство, но если вы создаете свое собственное свойство (а не заимствуете и перезаписываете биты родительского свойства), оно всегда будет удовлетворять требованиям ABC, даже если вы опускаете важныебиты.

3. Принято, поскольку я думаю, что я понимаю суть того, что здесь происходит, основываясь на этом объяснении. Спасибо… и @ShadowRanger для поиска отчета об ошибке.

Ответ №2:

@chepner ответил и хорошо объяснил это. Исходя из этого, я придумал способ обойти это, который есть… хорошо… решать вам. В лучшем случае подлый. Но это достигает моих 3 основных целей:

  1. Вызывает исключения для нереализованных установщиков в подклассах
  2. Поддерживает семантику свойств python (в сравнении с функциями и т. Д.)
  3. Позволяет избежать повторного объявления каждого свойства в каждом подклассе, которое все равно могло бы не решить # 1.

Просто объявите абстрактные функции get / set в базовом классе (не свойство). Затем добавьте инициализатор @classmethod в базовый класс, который создает фактические свойства с использованием этих абстрактных методов, но на этом этапе они будут конкретными методами в подклассе.

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

 from abc import abstractmethod, ABC

class ParentTask(object):
    def __init__(self):
        self.first_step = FirstStep(self)
        self.second_step = SecondStep(self)
        print(self.first_step.end)
        print(self.second_step.end)


class Step(ABC):
    def __init__(self, task):
        self.task = task

    @classmethod
    def init_properties(cls):
        cls.end = property(cls.get_end, cls.set_end)

    @abstractmethod
    def get_end(self):
        pass

    @abstractmethod
    def set_end(self, value):
        pass


class FirstStep(Step):
    def get_end(self):
        return 1

    def set_end(self, value):
        self.task.end = value


class SecondStep(Step):
    def get_end(self):
        return 2

    def set_end(self, value):
        self.task.end = value


FirstStep.init_properties()
SecondStep.init_properties()
ParentTask()