Рассмотрим следующий код:
from typing import Protocol, ClassVar, Any
class Tool(Protocol):
t: ClassVar
# def f(self, params: self.t) -> Any:
# NameError: name 'self' is not defined
def f(self, params) -> Any:
pass
class Sum:
t: ClassVar = list[int]
def f(self, params: list[int]) -> int:
return sum(params)
def use_tool(tool: Tool, params: Any) -> Any:
return tool.f(params)
s = Sum()
print(use_tool(s, [1, 2]))
Я хотел бы определить протокол Tool
так, чтобы такие классы, как Sum
, могли реализовать его с помощью определенного t
, который хранит объект типа/класса, чтобы в таких методах, как Sum.f()
, params
было бы ограничено использование типа t
при проверке типа. Как я могу это сделать?
Я пытался использовать self.t
для ссылки на t
в аннотации типа, но это не работает, равно как и замена self
на typing.Self
.
Я думаю, что сохранение типа параметра для f()
было бы полезно в том случае, если, предположим, t
хранит pydantic.BaseModel
(что означает, что t
имеет тип ModelMetaclass
), тогда в таких функциях, как use_tool
, я могу посмотреть схему t
через t.model_dump_json()
. Кроме того, use_tool()
может принимать строку JSON в качестве входных данных и анализировать ее в соответствующую модель через t.model_validate_json()
перед вызовом Tool.f
.
@Dunes Да, я подумывал о том, чтобы сделать Tool
универсальным, но я намерен сделать так, чтобы такие классы, как Sum
, реализовывали протокол Tool
, одновременно раскрывая Sum.t
. Я должен прояснить это в своем вопросе.
@oskar Первое твое предложение может показаться излишне резким. Предупреждение средств проверки типов о типе параметра/аргумента также считается «принудительным».
Кажется, вы можете использовать общий класс: docs.python.org/3.11/library/…
@oskar Я имел в виду «силу» в том смысле, в котором упоминался InSync. Я изменил свой вопрос, чтобы уточнить это. Это похоже на общие классы, где некоторые переменные должны иметь один и тот же тип.
@KamilCuk Я хотел бы также сохранить сам тип в классе. Было бы приемлемым решением, если бы Generic[T] позволял мне хранить сам T
, например let self.t = T
.
Извините, этого не должно было случиться. Вы можете использовать from __future__ import annotations
. Затем вы можете заменить типы строками, например. «Сумма». Возможно, это работает и в вашем случае с «Sum.t».
Чтобы использовать в качестве подсказки типа, t
должен быть TypeAlias
, но в этом случае он должен быть инициализирован одновременно с его определением:
(playground links: Mypy , Pyright)
class Tool(Protocol):
t: TypeAlias = list[int] # pyright => fine
# mypy => error: Type aliases are prohibited in protocol bodies
def f(self, params: 'Tool.t') -> Any: # both => fine
reveal_type(params) # both => list[int]
(Разрешены ли псевдонимы типов внутри Protocol
s, неясно; в спецификации об этом ничего не сказано.)
Вместо этого сделайте Tool
общим:
(playground links: Mypy , Pyright)
class Tool[T](Protocol):
t: type[T]
def f(self, params: T) -> Any: ...
Затем его можно использовать как:
class Sum:
# "type[list[int]]" is necessary for Mypy
# to understand that it is not a class-level type alias
t: type[list[int]] = list[int]
def f(self, params: list[int]) -> int: ...
def use_tool[T](tool: Tool[T], params: T) -> Any: ...
s = Sum()
use_tool(s, [1, 2]) # fine
У этого есть еще одно преимущество: Tool
также может быть универсальным по сравнению с возвращаемым типом .f()
.
(playground links: Mypy , Pyright)
class Tool[T, R](Protocol):
t: type[T]
def f(self, params: T) -> R: ...
...
def use_tool[T, R](tool: Tool[T, R], params: T) -> R: ...
s = Sum()
reveal_type(use_tool(s, [1, 2])) # int
Что касается части NameError
(которая не будет проблемой при таком решении), см. этот вопрос.
Вы не можете сделать это так, как предложено.
ClassVar
, как следует из названия, является переменной. Возможно, вы никогда не изменитеSum.t
, но любой инструмент статической типизации должен учитывать такую возможность. Вы рассматривали возможность созданияTool
дженерика?