Если функция возвращает подкласс 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
.
Отредактировал вопрос, чтобы разные подклассы ожидали разные аргументы в конструкторе.
Работает ли добавление реферата __init__
к Template
? (сам не пробовал)
Позвольте мне начать с подчеркивания того, что протоколы были введены специально, поэтому вам не нужно определять номинальный подкласс для создания отношения подтипа. Вот почему это называется структурным подтипом. Цитирую 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
был протоколом.
И все это не говорит о том, что невозможно найти приложения, где чему-то полезно быть и протоколом, и абстрактным базовым классом. Но такой вариант использования должен быть должным образом обоснован, и из контекста вашего вопроса я действительно не вижу для него никакого оправдания.
Зачем вам нужен один и тот же конструктор для двух подклассов?