Переписывая этот вопрос, потому что оказывается, что то, что я считал проблемой, не было проблемой. На самом деле, у меня есть, казалось бы, эквивалентный случай, на который mypy не жалуется.
Вот пример, когда нет жалоб на mypy, которые кажутся эквивалентными:
class TableLoader(ABC):
@property
@abstractmethod
def FieldToDataHeaderKey(self):
# dict of model dicts of field names and header keys
pass
def set_headers(self, custom_headers=None):
for mdl in self.FieldToDataHeaderKey.keys():
class StudyTableLoader(TableLoader):
FieldToDataHeaderKey = {
Study.__name__: {
"code": CODE_KEY,
"name": NAME_KEY,
"description": DESC_KEY,
},
}
Однако этот, казалось бы, эквивалентный код выдает ошибку mypy:
class PeakAnnotationsLoader(ABC):
@property
@abstractmethod
def add_columns_dict(self):
pass
@classmethod
def add_df_columns(cls, df_dict: dict):
for sheet, column_dict in cls.add_columns_dict.items():
# ^^^ This is the line that produces the error
...
class IsocorrLoader(PeakAnnotationsLoader):
add_columns_dict = {}
выдает следующие ошибки из mypy
:
DataRepo/loaders/peak_annotations_loader.py:213: error: "Callable[[PeakAnnotationsLoader], Any]" has no attribute "items" [attr-defined]
DataRepo/loaders/peak_annotations_loader.py:321: error: Need type annotation for "add_columns_dict" (hint: "add_columns_dict: Dict[<type>, <type>] = ...") [var-annotated]
Я не могу придумать, как осчастливить свою пию во втором случае. Весь этот код работает, кстати. Как мне удовлетворить свою py здесь?
Есть ли другой способ создания атрибутов абстрактного класса? У меня есть несколько классов, которые наследуются от TableLoader
, и ни один из них не выдает ошибок mypy WRT FieldToDataHeaderKey
. Просто mypy не справляется .items()
так, как .keys()
?
Возможно, вы правы, хотя сделать None, когда производному классу не нужно «добавлять столбцы», несложно. По сути, абстрактный класс делает все, но производному классу нужно только описать, как должно выполняться преобразование входных данных (диктата DataFrames) в универсальный формат. Это достигается путем определения атрибутов, описывающих, как должно выполняться преобразование. Метод, выполняющий преобразование, находится в базовом классе.
Полагаю, я мог бы поместить все в одну структуру данных, чтобы она всегда была определена, но это увеличило бы сложность и без того сложных структур данных. РН, при преобразовании используются 4 структуры данных: add_columns_dict
(эта), column_rename_dict
, drop_columns_list
и merge_dict
.
Или, я думаю, я мог бы сделать пустой вывод, что столбцов для добавления нет. Вероятно, это лучшее решение, но оно все равно кажется обходным путем. Похоже, объявление типа возвращаемого значения как необязательного должно работать.
«Абстрактный класс делает все». Вероятно, у вас должно быть несколько абстрактных классов, и конкретный класс может наследовать только те, которые ему нужны.
You should probably have multiple abstract classes
. ИМО, такой подход противоречит цели создания базового класса.
Другая проблема в том, что cls.add_columns_dict
, даже если и не None
, не является dict
; это property
объект. Вам необходимо получить доступ к свойству из экземпляра cls
, чтобы получить dict
, возвращаемый методом.
Я обнаруживаю это. Я попытался установить пустой словарь в производном классе, но все равно получаю ту же ошибку, поэтому, видимо, не понял, что происходит. Думаю, мне нужно научиться создавать атрибут абстрактного класса. Я делал это в других классах, но не с помощью dict... mypy не жалуется на другой, который у меня есть, это список...
ХОРОШО. Теперь я в замешательстве. Судя по всему, я делал это с помощью dict раньше, и mypy не жаловался на это, когда я использовал .keys()
в коде... Так запутался.
Собираюсь отредактировать свой вопрос, чтобы лучше определить проблему на основе того, что я только что узнал...
Re: в вашем обновлении вы «заменили» абстрактный метод фактическим dict
, а не методом получения свойства, которое «требуется» ABC.
@chepner - Простите, но я не понимаю, о чем вы говорите. Да, производный класс имеет атрибут класса с тем же именем, что и абстрактный метод. @Codrin сообщил мне, что добавление @property
заставляет производный класс иметь атрибут экземпляра (которому атрибут класса тайно удовлетворяет - если я правильно понимаю). Я ошибочно полагал, что свойство/абстрактный метод заставляет производный класс иметь атрибут класса с таким именем. Тем не менее, моим намерением было принудительное существование атрибута класса в производном классе, но ничего не изменило WRT в моем обновлении.
В классе PeakAnnotationsLoader
метод add_columns_dict
находится на уровне экземпляра, а метод add_df_columns
— на уровне класса. Использование cls.add_columns_dict
похоже на использование PeakAnnotationsLoaderChildClass.add_columns_dict
, которое теоретически возвращает то, что говорит mypy, — вызываемый объект.
Код работает, потому что вы переопределяете метод add_columns_dict
и делаете его статическим полем, поэтому PeakAnnotationsLoaderChildClass.add_columns_dict
возвращает значение этого поля. Родитель никоим образом не заставляет дочерние классы переопределять add_columns_dict
как таковой, и поэтому mypy выдает эту ошибку.
Ошибка исчезнет, если вы сделаете метод add_columns_dict
методом уровня экземпляра, удалив аннотацию @classmethod
.
Предполагаемый способ переопределить абстрактное свойство будет следующим:
class IsocorrLoader(PeakAnnotationsLoader):
@property
def add_columns_dict(self) -> Optional[Dict[str, Dict[str, function]]]:
...
Это сломает ваш код и больше не будет работать, поскольку вы больше не создаете статическое поле класса.
Насколько мне известно, невозможно иметь метод абстрактного класса, который одновременно является свойством.
Создание add_columns_dict
метода абстрактного класса вместо абстрактного свойства также может устранить эту ошибку. Тогда ваш код может выглядеть так:
class PeakAnnotationsLoader(ABC):
@classmethod
@abstractmethod
def get_add_columns_dict(self) -> Optional[Dict[str, Dict[str, function]]]:
pass
@classmethod
def add_df_columns(cls, df_dict: dict):
add_columns_dict = cls.get_add_columns_dict()
if add_columns_dict is not None:
for sheet, column_dict in add_columns_dict.items():
...
class IsocorrLoader(PeakAnnotationsLoader):
@classmethod
def get_add_columns_dict(self) -> Optional[Dict[str, Dict[str, function]]]:
...
Хммм... Моя цель состоит в том, чтобы производный класс определял только структуры данных, описывающие, как метод базового класса должен выполнять преобразование данных. Я считаю, что @property
и @abstractmethod
по сути требуют, чтобы производный класс определял эти атрибуты класса. У меня есть другой код, который делает это, и mypy не жалуется на это, поэтому здесь происходит что-то, чего я не понимаю, но ваши идеи о классе и экземпляре, возможно, являются частью головоломки. Сейчас я редактирую вопрос, чтобы добавить то, что я узнал.
Хорошо... Думаю, я медленно усваиваю твой ответ. Позвольте мне попробовать кое-что. Я думаю, вы что-то поняли, хотя я все же хочу, чтобы это был обязательный атрибут класса, определенный в производном классе через абстрактный базовый класс.
Я только что увидел ваше изменение, разница между двумя примерами заключается в том, что в первом примере у вас есть метод уровня экземпляра (def set_headers(self, ...)
), а во втором примере — метод уровня класса (def add_df_columns(cls, ...)
). Вы можете рассматривать методы класса как статические методы, поэтому во втором примере вы пытаетесь получить доступ к чему-то, чего не существует на уровне класса, к свойству, которое существует только после создания экземпляра. Код работает, потому что в дочернем классе вы берете метод экземпляра родительского класса и делаете его статическим методом дочернего класса.
Я использовал @property
(с @abstractmethod
) большую часть года и никогда не осознавал, что (и поправьте меня, если я ошибаюсь), по сути, это заставляло созданный экземпляр производного класса иметь атрибут экземпляра! Теперь эта ситуация имеет гораздо больше смысла. Я также понимаю, что нет чистого способа заставить его определить атрибут класса?
Это именно то! И, к сожалению, да, поскольку вы не можете использовать все три одновременно (@property
, @abstractmethod
и @classmethod
), вы не можете заставить производные классы иметь определенные атрибуты класса. Вы можете заставить их иметь методы класса, но не атрибуты класса. Проверьте это, если вам интересно обсуждение @classmethod
и @property
, которое было возможно в Python 3.9, но устарело в 3.11 и будет удалено в 3.13.
В итоге я решил эту проблему немного по-другому (с помощью рабочего кода, который удовлетворяет mypy, превратив методы в методы экземпляра), но именно ваш ответ помог мне понять, что происходит. Я понял, что мне не нужно, чтобы методы были методами класса. Я также недавно понял, что то, как я это делаю, — это своего рода трюк. Я, вероятно, еще немного поиграюсь с этим перед развертыванием. Большое спасибо за ваш ответ.
Делает ли
add_df_columns
что-нибудь, еслиadd_columns_dict
естьNone
? Я не думаю, что сделать абстрактное свойство необязательным — это решение.