Как добавить type hint для метода с типом внешнего класса в Python?
У меня есть следующий код в Python 3:
class Position:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
def __add__(self, other: Position) -> Position:
return Position(self.x + other.x, self.y + other.y)
Но мой редактор (PyCharm) говорит, что ссылка Position не может быть разрешена в методе __add__. Как мне указать, что ожидаемый тип возвращаемого значения должен быть типа Position?
Я думаю, что это может быть проблема PyCharm, так как он использует эту информацию в своих предупреждениях и автодополнении кода. Но поправьте меня, если я не прав и мне нужно использовать какой-то другой синтаксис.
Чтобы указать тип аннотацией для метода с типом класса, в котором он определен, в Python необходимо использовать ссылку вперед (forward reference), заключив имя класса в кавычки. В вашем случае правильный синтаксис будет:
class Position:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
def __add__(self, other: 'Position') -> 'Position':
return Position(self.x + other.x, self.y + other.y)
Кавычки вокруг 'Position' создают ссылку вперед, сообщая Python и проверщикам типов, что это относится к классу Position, даже на этапе обработки аннотации типов этот класс еще не полностью определен.
Содержание
- Проблема ссылок вперед (Forward Reference)
- Решение с использованием строковых кавычек
- Лучшие практики в зависимости от версии Python
- Обработка в проверщиках типов и IDE
- Альтернативные подходы
Проблема ссылок вперед (Forward Reference)
Когда Python обрабатывает определения классов, он выполняет код в теле класса сверху вниз. Когда он встречает аннотацию типа, такую как -> Position, он пытается немедленно разрешить имя Position. Однако на этом этапе выполнения класс Position все еще определяется и еще не полностью доступен в пространстве имен.
Это создает проблему ссылки вперед, когда проверщик типов (и среда выполнения Python) не может найти класс, на который ссылается в аннотации типа. Как объясняется в PEP 484:
Распространенным случаем использования ссылок вперед является ситуация, когда, например, в сигнатурах требуются модели Django. Обычно каждая модель находится в отдельном файле и имеет методы, принимающие аргументы, тип которых включает другие модели.
Та же проблема возникает внутри одного класса, когда вам нужно сослаться на сам класс в его собственных сигнатурах методов.
Решение с использованием строковых кавычек
Наиболее широко поддерживаемым решением является заключение имени класса в кавычки, создавая строковый литерал, который служит ссылкой вперед:
class Position:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
def __add__(self, other: 'Position') -> 'Position':
return Position(self.x + other.x, self.y + other.y)
def get_distance(self, other: 'Position') -> float:
return ((self.x - other.x)**2 + (self.y - other.y)**2)**0.5
Согласно документации Python:
Допустимо использовать строковые литералы в качестве части подсказки типа, например: class Tree: … def leaves(self) -> List[‘Tree’]: …
Этот подход работает, потому что:
- Интерпретатор Python рассматривает заключенную в кавычки строку как литерал и не пытается разрешить ее немедленно
- Проверщики типов, такие как mypy, понимают эту схему и разрешают ссылку вперед при анализе полного определения класса
- Поведение во время выполнения остается неизменным, поскольку подсказки типов обычно игнорируются во время выполнения (если их специально не оценивать)
Лучшие практики в зависимости от версии Python
Python 3.7 и ранее
Для версий Python 3.7 и ранее стандартным решением является подход с использованием строковых кавычек:
class Position:
def __add__(self, other: 'Position') -> 'Position':
# реализация метода
pass
Python 3.7+ с отложенной оценкой
Начиная с Python 3.7, вы можете включить отложенную оценку аннотаций с помощью импорта __future__. Это изменяет способ обработки аннотаций типов:
from __future__ import annotations
class Position:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
def __add__(self, other: Position) -> Position:
return Position(self.x + other.x, self.y + other.y)
Как объясняет Адам Джонсон:
Со старым поведением Python … ‘Widget’ не определен · Проверщики типов ввели обходной путь: обертывание подсказок типов в кавычки, чтобы превратить их в строки.
Функция from __future__ import annotations (реализованная в PEP 563) автоматически обрабатывает все аннотации типов как строки, устраняя необходимость в ручном добавлении кавычек в большинстве случаев.
Python 3.12+ и будущие возможности
Для Python 3.12 и будущих версий ведутся дискуссии о PEP 649, который позволил бы использовать имена классов напрямую без кавычек:
Обновление 2: Начиная с Python 3.10, PEP 563 пересматривается, и возможно, будет использован PEP 649 - он просто позволил бы использовать имя класса напрямую, без кавычек: в предложении pep говорится, что оно разрешается ленивым образом.
Однако на данный момент (и, вероятно, в обозримом будущем) подход с использованием строковых кавычек остается наиболее надежным решением.
Обработка в проверщиках типов и IDE
PyCharm и IntelliJ IDEA
Ваше предупреждение в PyCharm на самом деле является распространенным явлением, но это обычно всего лишь предупреждение статического анализа, а не фактическая ошибка времени выполнения. Современные версии PyCharm понимают ссылки вперед и должны правильно их разрешить после добавления кавычек.
Если вы все еще видите предупреждения после добавления кавычек, вы можете:
- Сбросить кэш и перезапустить PyCharm
- Убедиться, что вы используете последнюю версию PyCharm, поддерживающую функции Python 3.7+
- Проверить, что версия интерпретатора Python соответствует настройкам вашего проекта
Mypy
Mypy автоматически обрабатывает ссылки вперед. Согласно документации mypy:
Вы можете захотеть сослаться на класс до его определения. Это известно как “ссылка вперед” (forward reference).
Mypy разрешит заключенную в кавычки ссылку 'Position' на фактический класс Position при обработке полного определения класса.
Другие проверщики типов
Большинство современных проверщиков типов (включая Pyright, Pyre и т.д.) понимают шаблон ссылки вперед с кавычками. Если вы используете старый проверщик типов, вам может потребоваться использовать явное разрешение с помощью typing.get_type_hints().
Альтернативные подходы
Использование TypeVar для самоссылающихся типов
Для методов, которые возвращают экземпляры того же класса, можно использовать TypeVar:
from typing import TypeVar
T = TypeVar('T', bound='Position')
class Position:
def __add__(self, other: 'Position') -> T:
return Position(self.x + other.x, self.y + other.y)
Однако это более сложное решение, чем необходимо для простых случаев, и не дает значительных преимуществ по сравнению с подходом со строковыми кавычками.
Использование typing_extensions для Self
Python 3.11 представил тип Self, который специально предназначен для этого случая использования:
from typing import Self
class Position:
def __add__(self, other: 'Position') -> Self:
return Position(self.x + other.x, self.y + other.y)
Self - это специальный тип, представляющий “включающий класс” и разрешаемый во время выполнения. Это наиболее семантически правильный подход, когда он доступен.
Использование классовых методов с cls
Для классовых методов, которые возвращают экземпляры класса, можно использовать cls:
class Position:
@classmethod
def create_origin(cls) -> 'Position':
return cls(0, 0)
Источники
- PEP 484 – Type Hints - Официальное предложение по улучшению Python (PEP), охватывающее подсказки типов и ссылки вперед
- Документация Python по typing - Официальная документация Python по модулю typing
- Stack Overflow: Как указать тип аннотацией для метода с типом класса, в котором он определен? - Обсуждение сообщества с несколькими решениями
- Адам Джонсон: Python type hints: enable postponed evaluation with future.annotations - Подробное объяснение отложенной оценки
- Шпаргалка mypy по подсказкам типов - Практическое руководство по подсказкам типов, включая ссылки вперед
- GitHub issue #34 в Python typing - Обсуждение синтаксиса и семантики ссылок вперед
Заключение
Чтобы указать тип аннотацией для метода с типом класса, в котором он определен, в Python используйте ссылки вперед, заключив имя класса в кавычки:
class Position:
def __add__(self, other: 'Position') -> 'Position':
return Position(self.x + other.x, self.y + other.y)
Основные выводы:
- Строковые кавычки - универсальное решение - Они работают во всех версиях Python и с проверщиками типов
- Рассмотрите
from __future__ import annotations- Для Python 3.7+ это устраняет необходимость в ручном добавлении кавычек в большинстве случаев - Предупреждения PyCharm обычно являются ложными срабатываниями - Современные IDE понимают ссылки вперед и правильно их разрешают
- Для Python 3.11+ рассмотрите использование
Self- Это семантически более точно для самоссылающихся возвращаемых типов - Проверщики типов обрабатывают это автоматически - Дополнительная настройка не требуется для mypy, Pyright или подобных инструментов
Подход со строковыми кавычками является стандартным решением уже много лет и продолжает надежно работать во всех версиях Python и инструментах. Будущие версии могут упростить этот подход, но текущий решение хорошо протестировано и широко поддерживается.