Я хочу реализовать стандартный полиморфизм с помощью 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
@chepner Можете ли вы показать подходящий способ решить то, что я пытаюсь сделать? У меня что-то получилось с подклассами протоколов, но я не уверен, что следую лучшим практикам.
Например, нужно ли мне просто каждый раз выполнять явное приведение типов внутри переопределения подкласса?
То, что вы делаете, само по себе неуместно. Вы не можете ограничить типы аргументов в унаследованных методах.
Просто сделайте так, чтобы типы аргументов совпадали, либо ограничив область действия суперкласса, либо расширив область действия подкласса.
Теперь, как лучше всего извлечь поля, специфичные для типа подкласса? Например, «запрос» или «сайты» самым мифическим образом.
Ваш последний фрагмент с typevar и протоколом полностью корректен и безопасен. Я бы, вероятно, унаследовал от Tool[GoogleSearchOptions]
явно - это не является строго необходимым, но немного помогает проверке типов и дает вам больше свободы, поскольку любое несоответствие будет указано на сайте определения, а не на ошибках сайта использования с неявным подтипированием. Маленькая гнида: поскольку вы используете |
для объединений, значит, ваш уровень как минимум 3.10+, поэтому забудьте Dict
и List
и используйте их простые аналоги dict
и list
, которые параметризуются начиная с 3.9.
@STerliakov Спасибо, приятель, это полезно и имеет смысл.
Не добавляйте решение к вопросу; опубликуйте это как ответ.
Какова цель базового класса ContentPullOptions, если он фактически не предоставляет никакой абстракции или точек данных? Проблема здесь в том, что расширяющий класс имеет разные точки данных и поэтому несовместим с базовым классом в соответствии с правилами подтипирования, поэтому на самом деле нет способа «исправить» то, что вы сделали, кроме как удалить базовый класс. полностью. Есть хаки, которые помогут mypy не жаловаться..
Тип переопределенного метода, чтобы соответствовать принципу замены Лискова, должен быть подтипом унаследованного метода. Поскольку функции являются контравариантными по типам аргументов, это означает, что тип аргумента переопределенного метода должен быть супертипом унаследованного метода. То есть GoogleSearchTool.pull_content
должен принимать аргумент, по крайней мере, такой же общий, как ContentPullOptions
, а не что-то более конкретное.
Итак, у меня есть: ``` class GoogleSearchTool: async def pull_content( self, opts: ToolOptions, ) -> str | Dict[str, Any]: # Реализация здесь передает ``` Теперь мне нужно поле query
, поскольку оно специфично для этого типа экземпляра, приведение типов является единственным решением?
Решение Возможно, это все еще нарушает принцип Лискова.
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, это решение нормально и часто используется в дикой природе.
Изменение типа аргумента является нарушением принципа замены Лискова. Экземпляр
GoogleSearchTool
по-прежнему является экземпляромTool
, иTool.pull_content
по-прежнему ожидает, что любой экземплярContentPullOptions
будет принят в качестве аргумента. (Есть несколько подзаголовков, касающихся подклассовProtocol
, хотя я думаю, что они ортогональны и не имеют отношения к рассматриваемому вопросу.)