Как предоставить подсказку типа для функции, которая возвращает подкласс протокола в Python?

Если функция возвращает подкласс Protocol, какова рекомендуемая подсказка типа для возвращаемого типа этой функции?

Ниже приведен упрощенный фрагмент кода для представления

from typing import Protocol, Type
from abc import abstractmethod

class Template(Protocol):
    @abstractmethod
    def display(self) -> None:
        ...
        
class Concrete1(Template):
    def __init__(self, grade: int) -> None:
        self._grade = grade
        
    def display(self) -> None:
        print(f"Displaying {self._grade}")
        
class Concrete2(Template):
    def __init__(self, name: str) -> None:
        self._name = name
        
    def display(self) -> None:
        print(f"Printing {self._name}")
        
        
def give_class(type: int) -> Type[Template]:
    if type == 1:
        return Concrete1
    else:
        return Concrete2
    
    
concrete_class =  give_class(1)
concrete_class(5)

В строке concrete_class(5) Пайланс сообщает Expected no arguments to "Template" constructor.

Зачем вам нужен один и тот же конструктор для двух подклассов?

Nikolaj Š. 20.04.2023 17:50

Отредактировал вопрос, чтобы разные подклассы ожидали разные аргументы в конструкторе.

StevinWilson 20.04.2023 17:58

Работает ли добавление реферата __init__ к Template? (сам не пробовал)

Oli 20.04.2023 18:19
Почему в 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
3
108
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Протоколы — это не азбука

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

позволяя пользователям писать [...] код без явных базовых классов в определении класса.

Хотя вы можете явно создать подкласс протокола при определении конкретного класса, это не то, для чего они были разработаны.

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


Ваша ошибка и возможные решения

Что касается того, почему вы получаете эту ошибку, это легко объяснить. Ваш протокол Template не определяет собственный метод __init__. Когда объявлена ​​переменная типа type[Template] (т. е. класс, реализующий протокол Template) и вы хотите создать ее экземпляр, средство проверки типов увидит, что Template не определяет __init__, и вернется к object.__init__, который не принимает аргументов. Таким образом, предоставление аргумента конструктору корректно помечается как ошибка.

Поскольку вы хотите использовать свой протокол не только для аннотирования чистых экземпляров, которые следуют за ним, но и для классов, которые вы хотите создать (т. е. type[Template]), вам нужно подумать о методе __init__. Если вы хотите показать, что класс для реализации вашего Template протокола может иметь любой конструктор, вы должны включить в протокол такую ​​разрешительную __init__ сигнатуру, например:

class Template(Protocol):
    def __init__(self, *args: Any, **kwargs: Any) -> None: ...

Если вы хотите быть более конкретным/ограничительным, это, конечно, возможно. Например, вы можете объявить, что Template-совместимые классы должны принимать ровно один аргумент в своем __init__, но он может быть любого типа:

class Template(Protocol):
    def __init__(self, _arg: Any) -> None: ...

Оба этих решения будут работать в вашем примере. Последнее, однако, ограничит вас от передачи ключевого слова-аргумента конструктору с любым именем, кроме _arg, очевидно.


Правильное структурное подтипирование

В заключение я бы посоветовал вам правильно использовать возможности протоколов, чтобы разрешить структурное подтипирование и избавиться от явного подкласса и abstractmethod декораторов. Если все, что вам нужно, это довольно общий конструктор и ваш метод display, вы можете добиться этого следующим образом:

from typing import Any, Protocol


class Template(Protocol):
    def __init__(self, _arg: Any) -> None: ...

    def display(self) -> None: ...


class Concrete1:
    def __init__(self, grade: int) -> None:
        self._grade = grade

    def display(self) -> None:
        print(f"Displaying {self._grade}")


class Concrete2:
    def __init__(self, name: str) -> None:
        self._name = name

    def display(self) -> None:
        print(f"Printing {self._name}")


def give_class(type_: int) -> type[Template]:
    if type_ == 1:
        return Concrete1
    else:
        return Concrete2


concrete_class = give_class(1)
concrete_class(5)

Это проходит mypy --strict без ошибок (и также должно удовлетворять Pyright). Как видите, и Concrete1, и Concrete2 принимаются в качестве возвращаемых значений для give_class, потому что они оба следуют протоколу Template.


Правильное использование азбуки

Конечно, для абстрактных базовых классов все еще существуют допустимые приложения. Например, если вы хотите определить фактическую реализацию метода в вашем базовом классе, который сам вызывает абстрактный метод, явное подклассирование (номинальное подтипирование) может иметь смысл.

Пример:

from abc import ABC, abstractmethod
from typing import Any


class Template(ABC):
    @abstractmethod
    def __init__(self, _arg: Any) -> None: ...

    @abstractmethod
    def display(self) -> None: ...

    def call_display(self) -> None:
        self.display()


class Concrete1(Template):
    def __init__(self, grade: int) -> None:
        self._grade = grade

    def display(self) -> None:
        print(f"Displaying {self._grade}")


class Concrete2(Template):
    def __init__(self, name: str) -> None:
        self._name = name

    def display(self) -> None:
        print(f"Printing {self._name}")


def give_class(type_: int) -> type[Template]:
    if type_ == 1:
        return Concrete1
    else:
        return Concrete2


concrete_class = give_class(1)
obj = concrete_class(5)
obj.call_display()  # Displaying 5

Но это совсем другой вариант использования. Здесь у нас есть то преимущество, что Concrete1 и Concrete2 являются номинальными подклассами Template, таким образом, наследуют call_display от него. Поскольку они в любом случае являются номинальными подклассами, нет необходимости в том, чтобы Template был протоколом.

И все это не говорит о том, что невозможно найти приложения, где чему-то полезно быть и протоколом, и абстрактным базовым классом. Но такой вариант использования должен быть должным образом обоснован, и из контекста вашего вопроса я действительно не вижу для него никакого оправдания.

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