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

#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. Обратите внимание, что если в протоколе нет аргументов*, вы можете добавлять только аргументы ключевых слов, а если в протоколе нет аргументов**, вы можете добавлять только позиционные.