Как ввести подсказку для свойства абстрактного метода и сделать mypy счастливым?

Переписывая этот вопрос, потому что оказывается, что то, что я считал проблемой, не было проблемой. На самом деле, у меня есть, казалось бы, эквивалентный случай, на который 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()?

Делает ли add_df_columns что-нибудь, если add_columns_dict есть None? Я не думаю, что сделать абстрактное свойство необязательным — это решение.

chepner 16.05.2024 15:45

Возможно, вы правы, хотя сделать None, когда производному классу не нужно «добавлять столбцы», несложно. По сути, абстрактный класс делает все, но производному классу нужно только описать, как должно выполняться преобразование входных данных (диктата DataFrames) в универсальный формат. Это достигается путем определения атрибутов, описывающих, как должно выполняться преобразование. Метод, выполняющий преобразование, находится в базовом классе.

hepcat72 16.05.2024 15:48

Полагаю, я мог бы поместить все в одну структуру данных, чтобы она всегда была определена, но это увеличило бы сложность и без того сложных структур данных. РН, при преобразовании используются 4 структуры данных: add_columns_dict (эта), column_rename_dict, drop_columns_list и merge_dict.

hepcat72 16.05.2024 15:56

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

hepcat72 16.05.2024 15:58

«Абстрактный класс делает все». Вероятно, у вас должно быть несколько абстрактных классов, и конкретный класс может наследовать только те, которые ему нужны.

chepner 16.05.2024 15:59
You should probably have multiple abstract classes. ИМО, такой подход противоречит цели создания базового класса.
hepcat72 16.05.2024 16:00

Другая проблема в том, что cls.add_columns_dict, даже если и не None, не является dict; это property объект. Вам необходимо получить доступ к свойству из экземпляра cls, чтобы получить dict, возвращаемый методом.

chepner 16.05.2024 16:05

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

hepcat72 16.05.2024 16:09

ХОРОШО. Теперь я в замешательстве. Судя по всему, я делал это с помощью dict раньше, и mypy не жаловался на это, когда я использовал .keys() в коде... Так запутался.

hepcat72 16.05.2024 16:11

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

hepcat72 16.05.2024 16:15

Re: в вашем обновлении вы «заменили» абстрактный метод фактическим dict, а не методом получения свойства, которое «требуется» ABC.

chepner 16.05.2024 17:35

@chepner - Простите, но я не понимаю, о чем вы говорите. Да, производный класс имеет атрибут класса с тем же именем, что и абстрактный метод. @Codrin сообщил мне, что добавление @property заставляет производный класс иметь атрибут экземпляра (которому атрибут класса тайно удовлетворяет - если я правильно понимаю). Я ошибочно полагал, что свойство/абстрактный метод заставляет производный класс иметь атрибут класса с таким именем. Тем не менее, моим намерением было принудительное существование атрибута класса в производном классе, но ничего не изменило WRT в моем обновлении.

hepcat72 16.05.2024 17:45
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
12
65
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В классе 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 не жалуется на это, поэтому здесь происходит что-то, чего я не понимаю, но ваши идеи о классе и экземпляре, возможно, являются частью головоломки. Сейчас я редактирую вопрос, чтобы добавить то, что я узнал.

hepcat72 16.05.2024 16:22

Хорошо... Думаю, я медленно усваиваю твой ответ. Позвольте мне попробовать кое-что. Я думаю, вы что-то поняли, хотя я все же хочу, чтобы это был обязательный атрибут класса, определенный в производном классе через абстрактный базовый класс.

hepcat72 16.05.2024 16:32

Я только что увидел ваше изменение, разница между двумя примерами заключается в том, что в первом примере у вас есть метод уровня экземпляра (def set_headers(self, ...)), а во втором примере — метод уровня класса (def add_df_columns(cls, ...)). Вы можете рассматривать методы класса как статические методы, поэтому во втором примере вы пытаетесь получить доступ к чему-то, чего не существует на уровне класса, к свойству, которое существует только после создания экземпляра. Код работает, потому что в дочернем классе вы берете метод экземпляра родительского класса и делаете его статическим методом дочернего класса.

Codrin 16.05.2024 16:52

Я использовал @property@abstractmethod) большую часть года и никогда не осознавал, что (и поправьте меня, если я ошибаюсь), по сути, это заставляло созданный экземпляр производного класса иметь атрибут экземпляра! Теперь эта ситуация имеет гораздо больше смысла. Я также понимаю, что нет чистого способа заставить его определить атрибут класса?

hepcat72 16.05.2024 17:00

Это именно то! И, к сожалению, да, поскольку вы не можете использовать все три одновременно (@property, @abstractmethod и @classmethod), вы не можете заставить производные классы иметь определенные атрибуты класса. Вы можете заставить их иметь методы класса, но не атрибуты класса. Проверьте это, если вам интересно обсуждение @classmethod и @property, которое было возможно в Python 3.9, но устарело в 3.11 и будет удалено в 3.13.

Codrin 16.05.2024 17:11

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

hepcat72 16.05.2024 19:43

Другие вопросы по теме