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

410

У меня есть следующий код в 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) говорит, что ссылочная позиция не может быть разрешена (в __add__методе). Как мне указать, что я ожидаю, что возвращаемый тип будет иметь тип Position?

Изменить: я думаю, что это на самом деле проблема PyCharm. Он на самом деле использует информацию в своих предупреждениях и завершения кода

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

Майкл ван Гервен
источник

Ответы:

576

TL; DR : если вы используете Python 4.0, он просто работает. С сегодняшнего дня (2019) в 3.7+ вы должны включить эту функцию, используя оператор future ( from __future__ import annotations) - для Python 3.6 или ниже используйте строку.

Я думаю, вы получили это исключение:

NameError: name 'Position' is not defined

Это потому, что Positionнеобходимо определить, прежде чем вы сможете использовать его в аннотации, если вы не используете Python 4.

Python 3.7+: from __future__ import annotations

Python 3.7 представляет PEP 563: отложенная оценка аннотаций . Модуль, который использует оператор future, from __future__ import annotationsбудет автоматически сохранять аннотации в виде строк:

from __future__ import annotations

class Position:
    def __add__(self, other: Position) -> Position:
        ...

Это запланировано, чтобы стать по умолчанию в Python 4.0. Поскольку Python по-прежнему является языком с динамической типизацией, поэтому проверка типов во время выполнения не выполняется, ввод аннотаций не должен влиять на производительность, верно? Неправильно! Перед тем как питон 3.7, модуль типирование используется как один из самых медленных питона модулей в ядре , так если вы import typingувидите до 7 - кратного увеличения производительности при обновлении до 3.7.

Python <3.7: использовать строку

Согласно PEP 484 , вы должны использовать строку вместо самого класса:

class Position:
    ...
    def __add__(self, other: 'Position') -> 'Position':
       ...

Если вы используете инфраструктуру Django, это может быть знакомо, поскольку модели Django также используют строки для прямых ссылок (определения внешнего ключа, где внешняя модель еще объявлена selfили еще не объявлена). Это должно работать с Pycharm и другими инструментами.

источники

Соответствующие части PEP 484 и PEP 563 , чтобы избавить вас от поездки:

Прямые ссылки

Когда подсказка типа содержит имена, которые еще не определены, это определение может быть выражено как строковый литерал, который будет решен позже.

Ситуация, в которой это обычно происходит, - это определение класса контейнера, где определяемый класс встречается в сигнатуре некоторых методов. Например, следующий код (начало реализации простого двоичного дерева) не работает:

class Tree:
    def __init__(self, left: Tree, right: Tree):
        self.left = left
        self.right = right

Для решения этой проблемы мы пишем:

class Tree:
    def __init__(self, left: 'Tree', right: 'Tree'):
        self.left = left
        self.right = right

Строковый литерал должен содержать допустимое выражение Python (т. Е. Compile (lit, '', 'eval') должен быть допустимым объектом кода), и он должен вычисляться без ошибок после полной загрузки модуля. Локальное и глобальное пространство имен, в котором оно оценивается, должно быть теми же пространствами имен, в которых будут оцениваться аргументы по умолчанию для одной и той же функции.

и PEP 563:

В Python 4.0 аннотации функций и переменных больше не будут оцениваться во время определения. Вместо этого строковая форма будет сохранена в соответствующем __annotations__словаре. Проверщики статического типа не увидят никакой разницы в поведении, тогда как инструменты, использующие аннотации во время выполнения, должны будут отложить оценку.

...

Описанную выше функциональность можно включить начиная с Python 3.7, используя следующий специальный импорт:

from __future__ import annotations

Вещи, которые вы можете испытать вместо этого

А. Определить манекен Position

Перед определением класса поместите фиктивное определение:

class Position(object):
    pass


class Position(object):
    ...

Это избавит от NameErrorи может даже выглядеть хорошо:

>>> Position.__add__.__annotations__
{'other': __main__.Position, 'return': __main__.Position}

Но так ли это?

>>> for k, v in Position.__add__.__annotations__.items():
...     print(k, 'is Position:', v is Position)                                                                                                                                                                                                                  
return is Position: False
other is Position: False

Б. Обезьяна-патч для добавления аннотаций:

Возможно, вы захотите попробовать магию метапрограммирования на Python и написать декоратор, который исправит определение класса, чтобы добавить аннотации:

class Position:
    ...
    def __add__(self, other):
        return self.__class__(self.x + other.x, self.y + other.y)

Декоратор должен нести ответственность за эквивалент этого:

Position.__add__.__annotations__['return'] = Position
Position.__add__.__annotations__['other'] = Position

По крайней мере, это кажется правильным:

>>> for k, v in Position.__add__.__annotations__.items():
...     print(k, 'is Position:', v is Position)                                                                                                                                                                                                                  
return is Position: True
other is Position: True

Вероятно, слишком много проблем.

Вывод

Если вы используете 3.6 или ниже, используйте строковый литерал, содержащий имя класса, в 3.7 используйте, from __future__ import annotationsи это будет просто работать.

Пауло Скардин
источник
2
Правильно, это не проблема PyCharm, а проблема Python 3.5 PEP 484. Я подозреваю, что вы получите то же самое предупреждение, если будете запускать его с помощью инструмента mypy type.
Пол Эверитт
23
> Если вы используете Python 4.0, он, кстати, работает, вы видели Сару Коннор? :)
scrutari
@JoelBerkeley Я только что проверил это, и параметры типа работали для меня на 3.6, просто не забудьте импортировать из, typingпоскольку любой тип, который вы используете, должен находиться в области видимости при оценке строки.
Пауло Скардин
ах, моя ошибка, я только ''
объявлял
5
Важное примечание для всех, кто использует from __future__ import annotations- это должен быть импортирован до всех других импортов.
Артур
16

Указывать тип в виде строки хорошо, но всегда немного радует, что мы в основном обходим синтаксический анализатор. Так что лучше не писать ни одной из этих буквенных строк:

def __add__(self, other: 'Position') -> 'Position':
    return Position(self.x + other.x, self.y + other.y)

Небольшое изменение заключается в использовании связанного typevar, по крайней мере, тогда вы должны написать строку только один раз при объявлении typevar:

from typing import TypeVar

T = TypeVar('T', bound='Position')

class Position:

    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    def __add__(self, other: T) -> T:
        return Position(self.x + other.x, self.y + other.y)
vbraun
источник
8
Я бы хотел, чтобы Python typing.Selfуказывал это явно.
Александр Хуссах
2
Я пришел сюда, чтобы посмотреть, typing.Selfсуществовало ли что-то подобное вашему . Возвращение жестко закодированной строки не может вернуть правильный тип при использовании полиморфизма. В моем случае я хотел реализовать метод десериализации класса. Я решил вернуть диктет (kwargs) и позвонить some_class(**some_class.deserialize(raw_data)).
Скотт П.
Используемые здесь аннотации типов подходят для правильной реализации, чтобы использовать подклассы. Однако реализация возвращает Position, а не класс, поэтому приведенный выше пример технически некорректен. Реализация должна заменить Position(что-то вроде self.__class__(.
Сэм Булл
Кроме того, в аннотациях говорится, что тип возвращаемого значения зависит other, но, скорее всего, это зависит от self. Таким образом, вам нужно поместить аннотацию selfдля описания правильного поведения (и, возможно, otherпросто Positionпоказать, что она не связана с типом возвращаемого значения). Это также может быть использовано в тех случаях, когда вы работаете только с self. напримерdef __aenter__(self: T) -> T:
Sam Bull
15

Имя «Позиция» недоступно во время анализа самого тела класса. Я не знаю, как вы используете объявления типов, но PEP 484 в Python - это то, что должен использовать большинство режимов, если с помощью этих подсказок о наборе текста говорят, что вы можете просто поместить имя в виде строки на этом этапе:

def __add__(self, other: 'Position') -> 'Position':
    return Position(self.x + other.x, self.y + other.y)

Проверьте https://www.python.org/dev/peps/pep-0484/#forward-references - инструменты, соответствующие этому, будут знать, как развернуть имя класса оттуда и использовать его. (Всегда важно иметь помните, что сам язык Python ничего не делает из этих аннотаций - они обычно предназначены для анализа статического кода, или можно иметь библиотеку / среду для проверки типов во время выполнения - но вы должны явно установить это).

Обновление Кроме того, начиная с Python 3.8, проверьте pep-563 - начиная с Python 3.8 можно писать, from __future__ import annotationsчтобы отложить оценку аннотаций - классы прямой ссылки должны работать просто.

jsbueno
источник
9

Когда подсказка типа на основе строки является приемлемой, __qualname__элемент также может быть использован. Он содержит имя класса и доступен в теле определения класса.

class MyClass:
    @classmethod
    def make_new(cls) -> __qualname__:
        return cls()

Делая это, переименование класса не подразумевает изменение подсказок типа. Но я лично не ожидал бы, что умные редакторы кода будут хорошо обрабатывать эту форму.

Ивон ДУТАПИС
источник
1
Это особенно полезно, потому что оно не кодирует имя класса жестко, поэтому оно продолжает работать в подклассах.
Флориан Брукер
Я не уверен, будет ли это работать с отложенной оценкой аннотаций (PEP 563), поэтому я задал вопрос для этого .
Флориан Брукер