Как использовать тип, хранящийся в переменной класса, в качестве подсказки типа для параметра метода того же класса?

Рассмотрим следующий код:

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.

Вы не можете сделать это так, как предложено. ClassVar, как следует из названия, является переменной. Возможно, вы никогда не измените Sum.t, но любой инструмент статической типизации должен учитывать такую ​​возможность. Вы рассматривали возможность создания Tool дженерика?

Dunes 25.07.2024 10:41

@Dunes Да, я подумывал о том, чтобы сделать Tool универсальным, но я намерен сделать так, чтобы такие классы, как Sum, реализовывали протокол Tool, одновременно раскрывая Sum.t. Я должен прояснить это в своем вопросе.

Tom Lin 25.07.2024 10:46

@oskar Первое твое предложение может показаться излишне резким. Предупреждение средств проверки типов о типе параметра/аргумента также считается «принудительным».

InSync 25.07.2024 10:51

Кажется, вы можете использовать общий класс: docs.python.org/3.11/library/…

Yurii Motov 25.07.2024 10:58

@oskar Я имел в виду «силу» в том смысле, в котором упоминался InSync. Я изменил свой вопрос, чтобы уточнить это. Это похоже на общие классы, где некоторые переменные должны иметь один и тот же тип.

Tom Lin 25.07.2024 10:59

@KamilCuk Я хотел бы также сохранить сам тип в классе. Было бы приемлемым решением, если бы Generic[T] позволял мне хранить сам T, например let self.t = T.

Tom Lin 25.07.2024 11:02

Извините, этого не должно было случиться. Вы можете использовать from __future__ import annotations. Затем вы можете заменить типы строками, например. «Сумма». Возможно, это работает и в вашем случае с «Sum.t».

oskar 25.07.2024 11:03
Почему в 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
7
55
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Чтобы использовать в качестве подсказки типа, 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]

(Разрешены ли псевдонимы типов внутри Protocols, неясно; в спецификации об этом ничего не сказано.)

Вместо этого сделайте 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 (которая не будет проблемой при таком решении), см. этот вопрос.

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