#python #protocols #type-hinting #mypy #python-typing
Вопрос:
Мой вопрос прост. У меня есть этот протокол:
from typing import Protocol
class LauncherTemplateConfig(Protocol):
def launch_program_cmd(self, **kwargs) -> list[str]:
pass
И эта реализация протокола, которую я ожидал бы, что mypy пройдет, но это не так:
from typing import Optional
from pathlib import Path
class MyLauncherTemplateConfig:
def launch_program_cmd(
self, some_arg: Optional[Path] = None, another_arg=1
) -> list[str]:
Я бы ожидал, что параметры в MyLauncherTemplateConfig.launch_program_cmd
будут совместимы с **kwargs
классом протокола.
Не уверен, что делаю что-то не так…
Ответ №1:
Общий принцип
Если вы хотите , чтобы MyPy признал, что определенный класс реализует интерфейс, определенный в a Protocol
, соответствующий метод в конкретной реализации должен быть не менее разрешающим в аргументах, которые он будет принимать, чем абстрактная версия этого метода, определенная в Protocol
. Это согласуется с другими принципами объектно-ориентированного программирования, такими как принцип подстановки Лискова.
Конкретная проблема здесь
Вы Protocol
определяете интерфейс, в котором launch_program_cmd
метод может быть вызван с любыми ключевыми словами-аргументами и не завершаться ошибкой во время выполнения. Ваша конкретная реализация не удовлетворяет этому интерфейсу, так как любые аргументы ключевых слов, отличные от some_arg
или another_arg
приведут к возникновению ошибки в методе.
Возможное решение
Если вы хотите , чтобы MyPy объявил ваш класс безопасной реализацией вашего Protocol
, у вас есть два варианта. Вы можете либо настроить подпись метода в Protocol
более конкретной реализации, либо настроить подпись метода в конкретной реализации, чтобы она была более общей. В последнем случае вы можете сделать это следующим образом:
from typing import Any, Protocol, Optional
from pathlib import Path
class LauncherTemplateConfig(Protocol):
def launch_program_cmd(self, **kwargs: Any) -> list[str]: ...
class MyLauncherTemplateConfig:
def launch_program_cmd(self, **kwargs: Any) -> list[str]:
some_arg: Optional[Path] = kwargs.get('some_arg')
another_arg: int = kwargs.get('another_arg', 1)
# and then the rest of your method
Используя этот dict.get
метод, мы можем сохранить значения по умолчанию в вашей реализации, но сделать это таким образом, чтобы придерживаться общей подписи метода, объявленной в Protocol
Комментарии:
1. Спасибо. Это тот путь, по которому я собирался пойти. Я соглашусь, так как другого способа нет, так как подписи на самом деле несколько «статичны». В противном случае это нарушило бы этот принцип, да.
Ответ №2:
Ключевая проблема в том, что ваша вседозволенность перевернута с ног на голову. Формально это происходит потому, что функции являются контравариантными по своим входным данным.
Вы обещаете, что def launch_program_cmd(self, **kwargs) -> list[str]:
«этот метод сможет принимать любой набор аргументов ключевых слов».
В качестве примера, если кто-то напишет
def launch_lunch(launcher: LauncherTemplateConfig):
launcher.launch_program_cmd(food=["eggs", "spam", "spam"])
тогда, согласно определению LauncherTemplateConfig
, это должно быть разрешено.
Но если вы попытаетесь вызвать этот метод с экземпляром MyLauncherTemplateConfig
, то он выйдет из строя, потому что он не знает, что делать с food
параметром. Таким MyLauncherTemplateConfig
образом, не является допустимым подтипом LauncherTemplateConfig
Я подозреваю, что то, что вы намереваетесь передать, больше похоже на «Этот метод будет существовать, но я не знаю, какие аргументы для этого потребуются». Однако на самом деле это не то, для чего настроен MyPy. Основная причина в том, что это не очень полезно: вы мало что можете сделать, пообещав, что метод будет существовать, но вы не знаете, как его назвать!
(Примечание: допускается противоположное направление. Если бы в вашем протоколе было указано, что вы должны иметь возможность принимать some_arg
и another_arg
, и ваша реализация была способна обрабатывать что угодно вообще, это было бы разрешено. Но в целом вы хотели бы, чтобы ваш протокол указывал вам, что вы на самом деле хотите предпринять.)
Комментарии:
1. На самом деле я не знаю аргументов, поэтому я использую **кварги в базовом классе, что означает: «мой класс, реализующий протокол, имеет подпись с любым количеством ключевых аргументов». Они проверяются во время выполнения позже, так как я использую эти аргументы динамически.
2. Тогда это твоя проблема. Если вы говорили, что реализующий класс может справиться с чем угодно, с вами все будет в порядке. Что-то вроде
def launch_program_cmd(self, *args: Any, **kwargs: Any):
в Протоколе иdef launch_program_cmd(self, some_arg: Optional[Path] = None, another_arg=1, *args: Any, some_kwarg: float=1.2, **kwargs: Any):
в реализации.3. Обратите внимание, что если в протоколе нет аргументов*, вы можете добавлять только аргументы ключевых слов, а если в протоколе нет аргументов**, вы можете добавлять только позиционные.