Як я можу вказати, що тип повернення методу такий самий, як і сам клас?


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. Він фактично використовує інформацію у своїх попередженнях та заповненні коду

Але виправте мене, якщо я помиляюся, і потрібно використовувати якийсь інший синтаксис.

Відповіді:


574

TL; DR : якщо ви використовуєте Python 4.0, він просто працює. На сьогодні (2019) у версії 3.7+ ви повинні увімкнути цю функцію за допомогою майбутнього оператора ( 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: відкладене оцінювання приміток . Модуль, який використовує майбутній оператор, from __future__ import annotationsавтоматично зберігатиме анотації як рядки:

from __future__ import annotations

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

Це планується стати типовим у Python 4.0. Оскільки Python все ще є динамічно набраною мовою, тому перевірка типу не виконується під час виконання, введення анотацій не повинно впливати на ефективність, правда? Неправильно! Перед python 3.7 модуль набору тексту був одним із найповільніших модулів python в ядрі, тож якщо ви import typingпобачите, що під час оновлення до 3.7 ви збільшите продуктивність до 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 (тобто компіляція (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

B. Патч мавп, щоб додати примітки:

Ви можете спробувати трохи магії програмування 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.
Пол Еверітт

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:

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)

8
Я б хотів, щоб у Python typing.Selfце було чітко вказано.
Олександр Хузаг

2
Я прийшов сюди, щоб подивитися, чи існує щось подібне до вас typing.Self. Повернення жорстко закодованого рядка не може повернути правильний тип при використанні поліморфізму. У моєму випадку я хотів реалізувати метод десеріалізації класу. Я погодився повернути дікт (кваргс) і подзвонити some_class(**some_class.deserialize(raw_data)).
Скотт П.

Анотації типу, що використовуються тут, доцільно при правильній реалізації цього для використання підкласів. Однак реалізація повертає Position, а не клас, тому приклад вище є технічно неправильним. Впровадження має замінити Position(щось подібне self.__class__(.
Сем Булл

Крім того, в анотаціях йдеться про те, що тип повернення залежить від цього other, але, швидше за все, саме від цього залежить self. Отже, вам потрібно буде помістити анотацію, selfщоб описати правильну поведінку (і, можливо, otherслід просто Positionпоказати, що вона не прив’язана до типу повернення). Це також можна використовувати у випадках, коли ви працюєте лише з ними self. наприкладdef __aenter__(self: T) -> T:
Сем Булл

15

Назва "Позиція" не доступна під час розбору тіла класу. Я не знаю, як ви використовуєте декларації типу, але Python's PEP 484 - саме для цього слід користуватися більшості режимів, якщо використовуючи ці підказки набору тексту, можна сказати, що ви можете просто поставити ім'я як рядок у цій точці:

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 не робить нічого з цих анотацій - вони зазвичай призначені для аналізу статичного коду, або можна мати бібліотеку / рамку для перевірки типів під час виконання, але ви повинні це чітко встановити).

update Також, як і для Python 3.8, перевірте pep-563 - на Python 3.8 можна написати, from __future__ import annotationsщоб відкласти оцінку анотацій - класи прямих посилань повинні працювати прямо.


9

Коли натяк на основі рядка є прийнятним, __qualname__елемент можна також використовувати. Він містить назву класу, і він доступний в тілі визначення класу.

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

Роблячи це, перейменування класу не означає зміни типів підказки. Але я особисто не очікував, що редактори розумних кодів добре впораються з цією формою.


1
Це особливо корисно, оскільки воно не жорстко кодує назву класу, тому він продовжує працювати в підкласах.
Флоріан

Я не впевнений, чи буде це працювати з відкладеною оцінкою анотацій (PEP 563), тому я поставив питання для цього .
Флоріан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.