Наследование и полиморфизм в Python при использовании mypy не работает

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

Базовый класс

class ContentPullOptions:
    pass


class Tool(Protocol):
    async def pull_content(self, opts: ContentPullOptions) -> str | Dict[str, Any]: ...

Подкласс

class GoogleSearchOptions(ContentPullOptions):
    query: str
    sites: List[str]


class GoogleSearchTool(Tool):
    async def pull_content(
        self,
        opts: GoogleSearchOptions,
    ) -> str | Dict[str, Any]:

Не удается:

error: Argument 1 of "pull_content" is incompatible with supertype "Tool"; supertype defines the argument type as "ContentPullOptions" 

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

Я пробовал пользовательские типы, приведение типов и т. д. Но все было немного запутанно и неясно.

Решение

Возможно, это все еще нарушает принцип Лискова.

from typing import TypeVar, Protocol, Dict, Any, List, Callable

# Define T as contravariant
T_contra = TypeVar('T_contra', bound=ContentPullOptions, contravariant=True)

class ContentPullOptions:
    pass

class Tool(Protocol[T_contra]):
    async def pull_content(self, opts: T_contra) -> str | Dict[str, Any]: ...

class GoogleSearchOptions(ContentPullOptions):
    query: str
    sites: List[str]

class GoogleSearchTool:
    async def pull_content(
        self,
        opts: GoogleSearchOptions,
    ) -> str | Dict[str, Any]:
        # Implementation here
        pass

Изменение типа аргумента является нарушением принципа замены Лискова. Экземпляр GoogleSearchTool по-прежнему является экземпляром Tool, и Tool.pull_content по-прежнему ожидает, что любой экземпляр ContentPullOptions будет принят в качестве аргумента. (Есть несколько подзаголовков, касающихся подклассов Protocol, хотя я думаю, что они ортогональны и не имеют отношения к рассматриваемому вопросу.)

chepner 24.07.2024 00:04

@chepner Можете ли вы показать подходящий способ решить то, что я пытаюсь сделать? У меня что-то получилось с подклассами протоколов, но я не уверен, что следую лучшим практикам.

Ryan 24.07.2024 00:08

Например, нужно ли мне просто каждый раз выполнять явное приведение типов внутри переопределения подкласса?

Ryan 24.07.2024 00:09

То, что вы делаете, само по себе неуместно. Вы не можете ограничить типы аргументов в унаследованных методах.

chepner 24.07.2024 00:09

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

Andrew Yim 24.07.2024 00:10

Теперь, как лучше всего извлечь поля, специфичные для типа подкласса? Например, «запрос» или «сайты» самым мифическим образом.

Ryan 24.07.2024 00:12

Ваш последний фрагмент с typevar и протоколом полностью корректен и безопасен. Я бы, вероятно, унаследовал от Tool[GoogleSearchOptions] явно - это не является строго необходимым, но немного помогает проверке типов и дает вам больше свободы, поскольку любое несоответствие будет указано на сайте определения, а не на ошибках сайта использования с неявным подтипированием. Маленькая гнида: поскольку вы используете | для объединений, значит, ваш уровень как минимум 3.10+, поэтому забудьте Dict и List и используйте их простые аналоги dict и list, которые параметризуются начиная с 3.9.

STerliakov 24.07.2024 03:07

@STerliakov Спасибо, приятель, это полезно и имеет смысл.

Ryan 24.07.2024 06:15

Не добавляйте решение к вопросу; опубликуйте это как ответ.

chepner 24.07.2024 15:06

Какова цель базового класса ContentPullOptions, если он фактически не предоставляет никакой абстракции или точек данных? Проблема здесь в том, что расширяющий класс имеет разные точки данных и поэтому несовместим с базовым классом в соответствии с правилами подтипирования, поэтому на самом деле нет способа «исправить» то, что вы сделали, кроме как удалить базовый класс. полностью. Есть хаки, которые помогут mypy не жаловаться..

Raggamuffin 24.07.2024 21:18
Почему в 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 может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
10
59
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Тип переопределенного метода, чтобы соответствовать принципу замены Лискова, должен быть подтипом унаследованного метода. Поскольку функции являются контравариантными по типам аргументов, это означает, что тип аргумента переопределенного метода должен быть супертипом унаследованного метода. То есть GoogleSearchTool.pull_content должен принимать аргумент, по крайней мере, такой же общий, как ContentPullOptions, а не что-то более конкретное.

Итак, у меня есть: ``` class GoogleSearchTool: async def pull_content( self, opts: ToolOptions, ) -> str | Dict[str, Any]: # Реализация здесь передает ``` Теперь мне нужно поле query, поскольку оно специфично для этого типа экземпляра, приведение типов является единственным решением?

Ryan 24.07.2024 00:13
Ответ принят как подходящий

Решение Возможно, это все еще нарушает принцип Лискова.

from typing import TypeVar, Protocol, Dict, Any, List, Callable

# Define T as contravariant
T_contra = TypeVar('T_contra', bound=ContentPullOptions, contravariant=True)

class ContentPullOptions:
    pass

class Tool(Protocol[T_contra]):
    async def pull_content(self, opts: T_contra) -> str | Dict[str, Any]: ...

class GoogleSearchOptions(ContentPullOptions):
    query: str
    sites: List[str]

class GoogleSearchTool:
    async def pull_content(
        self,
        opts: GoogleSearchOptions,
    ) -> str | Dict[str, Any]:
        # Implementation here
        pass

Нет, общая параметризация не нарушает LSP, это решение нормально и часто используется в дикой природе.

STerliakov 24.07.2024 21:14

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