#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. Упрощенный пример с juststart
, no*end
и пустым определениемFirstStep
raises , которое я получаюTypeError: Can't instantiate abstract class FirstStep with abstract methods start
. Это работает с 3.3.2. О, я понимаю. Вы определили средство получения, но не задающее средство, и вы хотите, чтобы требовались как средство получения, так и средство установки. Игнорируйте меня.
3. Да, ответ @chepner, похоже, объясняет эту часть. Я все еще пытаюсь переварить остальное.
Ответ №1:
Я подозреваю, что это ошибка во взаимодействии абстрактных методов и свойств.
В вашем базовом классе происходят следующие вещи, в порядке:
- Вы определяете абстрактный метод с именем
start
. - Вы создаете новое свойство, которое использует абстрактный метод из 1) в качестве его получателя. Теперь имя
start
ссылается на это свойство, и единственная ссылка на исходное имя теперь хранитсяSelf.start.fget
. - Python сохраняет временную ссылку на
start.setter
, потому что имяstart
будет привязано к еще одному объекту. - Вы создаете второй абстрактный метод с именем
start
- Ссылке из 3) присваивается абстрактный метод из 4) для определения нового свойства для замены однажды связанного с именем
start
. Это свойство имеет в качестве получателя метод из 1 и в качестве установщика метод из 4). Теперьstart
ссылается на это свойство;start.fget
ссылается на метод из 1);start.fset
ссылается на метод из 4).
На данный момент у вас есть свойство, компоненты которого являются абстрактными методами. Само свойство не было оформлено как абстрактное, но определение property.__isabstractmethod__
помечает его как таковое, потому что все его составляющие методы являются абстрактными. Что еще более важно, у вас есть следующие записи в Step.__abstractmethods__
:
start
,property
end
,property
set_end
, установщик дляend
gen_end
, получатель дляend
Обратите внимание, что функции компонента для start
свойства отсутствуют, поскольку __abstractmethods__
хранятся имена, а не ссылки на вещи, которые необходимо переопределить. Использование property
и метод результирующего свойства setter
в качестве декораторов многократно заменяют то, на что start
ссылается имя.
Теперь в вашем дочернем классе вы определяете новое свойство с именем start
, затеняющее унаследованное свойство, у которого нет установщика и конкретного метода в качестве его получателя. На данный момент не имеет значения, предоставляете ли вы сеттер для этого свойства или нет, потому что, что касается abc
оборудования, вы предоставили все, что он просил:
- Конкретный метод для имени
start
- Конкретные методы для имен
get_end
иset_end
- Неявно конкретное определение для имени
end
, потому что всем базовым функциям для свойстваend
были предоставлены конкретные определения.
Комментарии:
1. В документах для
abstractproperty
(устаревших) документируется подход замены только@abstractmethod
на@property
. В приведенном примере прямо указано, что свойства могут быть частично абстрактными (например, только установщик, а не получатель в их примере) и что вам нужно перезаписать только эту частьproperty
(подразумевая, что если у вас есть несколько абстрактных компонентов, вы должны перезаписать все абстрактные компоненты), тот факт, что на самом деле это не тактакое поведение определенно представляет собой ошибку.2. По-видимому, для этого есть закрытая ошибка . Похоже, что смысл, который я там прочитал, на самом деле не существует; вы можете перезаписать отдельные элементы, ссылаясь на родительское свойство, но если вы создаете свое собственное свойство (а не заимствуете и перезаписываете биты родительского свойства), оно всегда будет удовлетворять требованиям ABC, даже если вы опускаете важныебиты.
3. Принято, поскольку я думаю, что я понимаю суть того, что здесь происходит, основываясь на этом объяснении. Спасибо… и @ShadowRanger для поиска отчета об ошибке.
Ответ №2:
@chepner ответил и хорошо объяснил это. Исходя из этого, я придумал способ обойти это, который есть… хорошо… решать вам. В лучшем случае подлый. Но это достигает моих 3 основных целей:
- Вызывает исключения для нереализованных установщиков в подклассах
- Поддерживает семантику свойств python (в сравнении с функциями и т. Д.)
- Позволяет избежать повторного объявления каждого свойства в каждом подклассе, которое все равно могло бы не решить # 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()