Оценивает ли Python подсказку типа прямой ссылки?

Я просматривал раздел PEP 484 в разделе Forward References и заметил заявление:

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

И это заставило меня задуматься, когда «позже» и чем? Интерпретатор не пытается разрешить его как литерал позже, так что же делать? Это просто, если для этого написан сторонний инструмент?

Небольшой пример для демонстрации результата интерпретатора:

class A:
    def test(self, a: 'A') -> None:
        pass
class B:
    def test(self, a: A) -> None:
        pass

>>> A().test.__annotations__
{'a': 'A', 'return': None}
>>> B().test.__annotations__
{'a': <class '__main__.A'>, 'return': None}

Если мое понимание аннотаций функций и подсказок типов правильное, Python на самом деле не ничего делает с ними во время выполнения для повышения производительности, а, скорее, интроспективное использование позволяет строго третьей стороне приложения, такие как линтеры, IDE и инструменты статического анализа (такие как mypy), чтобы воспользоваться их доступностью. Так будут ли эти инструменты пытаться разрешить подсказку типа 'A' вместо того, чтобы возлагать эту работу на интерпретатор, и если да, то как они это делают?

Обновление:

С помощью модуля typing пользовательский код может выполнять следующие действия:

>>> typing.get_type_hints(A().test)
{'a': <class '__main__.A'>, 'return': <class 'NoneType'>}
>>> typing.get_type_hints(B().test)
{'a': <class '__main__.A'>, 'return': <class 'NoneType'>}

Однако мой вопрос направлен на то, несет ли Python какую-либо ответственность за обновление __annotations__ функции из строкового литерала, то есть при изменении времени выполнения:

>>> A().test.__annotations__
{'a': 'A', 'return': None}

to...

>>> A().test.__annotations__
{'a': <class '__main__.A'>, 'return': None}

Если Python этого не делает, то зачем мне строковый литерал в качестве подсказки типа, кроме самодокументированного кода? Какую ценность дает мне первая форма, пользователю или стороннему инструменту?


person pasta_sauce    schedule 24.03.2019    source источник
comment
mypy даже не запускает ваш код. Он никак не мог бы делегировать разрешение подсказки типа механизмам среды выполнения Python, потому что время выполнения не происходит. Это верно независимо от того, используете ли вы строковые аннотации или нет; он должен сделать свое собственное решение, несмотря ни на что.   -  person user2357112 supports Monica    schedule 24.03.2019
comment
@user2357112 user2357112 Итак, каким образом помогает использование строкового литерала (просто чтобы избежать проблем с прямой ссылкой при определении)? Это просто самодокументированный код? Способ сказать, что я бы намекнул здесь, но у меня была прямая ссылка, поэтому вместо этого я использовал строку? Почему бы никогда не использовать строковый литерал, если это так? Например, с какой целью подсказка с фактическим классом дает преимущество по сравнению с простой строкой? Я вижу это только в том случае, если сторонний код пытается оценить строку.   -  person pasta_sauce    schedule 24.03.2019
comment
@user2357112 user2357112 Очевидно, что в этой проблеме есть улучшения по сравнению с 3.7, однако я пытаюсь понять, когда меня заставляют использовать строковый литерал вместо имени класса, что я теряю/получаю от этого?   -  person pasta_sauce    schedule 24.03.2019


Ответы (1)


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

class Foo:
    def bar(self) -> Foo:
        return Foo()

Эта программа на самом деле рухнет во время выполнения, если вы попытаетесь запустить ее с помощью Python: когда интерпретатор увидит определение bar, определение Foo еще не закончено. Итак, поскольку Foo еще не добавлено в глобальное пространство имен, мы пока не можем использовать его как подсказку типа.

Аналогично рассмотрим эту программу:

class Foo:
    def bar(self) -> Bar:
        return Bar()

class Bar:
    def foo(self) -> Foo:
        return Foo()

Это взаимозависимое определение страдает от той же проблемы: пока мы оцениваем Foo, Bar еще не вычислено, поэтому интерпретатор выдает исключение.


Есть три решения этой проблемы. Во-первых, сделать некоторые из ваших подсказок типа строками, фактически «объявив» их вперед:

class Foo:
    def bar(self) -> "Foo":
        return Foo()

Это удовлетворяет интерпретатору Python и не нарушит работу сторонних инструментов, таких как mypy: они могут просто удалить кавычки перед синтаксическим анализом типа. Главный недостаток заключается в том, что этот синтаксис выглядит несколько уродливым и неуклюжим.

Второе решение — использовать синтаксис комментариев типа:

class Foo:
    def bar(self):
        # type: () -> Foo
        return Foo()

Это имеет те же преимущества и недостатки, что и первое решение: оно удовлетворяет интерпретатору и инструментарию, но выглядит хакерским и уродливым. У него также есть дополнительное преимущество, заключающееся в том, что он поддерживает обратную совместимость вашего кода с Python 2.7.

Третье решение — только для Python 3.7+ — используйте директиву from __future__ import annotations:

from __future__ import annotations 

class Foo:
    def bar(self) -> Foo:
        return Foo()

Это автоматически сделает все аннотации представленными в виде строк. Таким образом, мы получаем преимущество первого решения, но без уродства.

Это поведение в конечном итоге станет поведением по умолчанию в будущих версиях Python.

Также оказывается, что автоматическое преобразование всех строк аннотаций может привести к некоторым улучшениям производительности. Создание таких типов, как List[Dict[str, int]], может оказаться на удивление дорогим: во время выполнения они представляют собой просто регулярные выражения и оцениваются так, как если бы они были написаны как List.__getitem__(Dict.__getitem__((str, int)).

Вычисление этого выражения несколько затратно: в итоге мы выполняем вызовы двух методов, конструируем кортеж и конструируем два объекта. Конечно, это не считая дополнительной работы, выполняемой в самих методах __getitem__, а работа, выполняемая в этих методах, оказывается нетривиальной по необходимости.

(Короче говоря, им нужно создавать специальные объекты, которые гарантируют, что типы, подобные List[int], не могут быть использованы ненадлежащим образом во время выполнения — например, в проверках isinstance и т.п.)

person Michael0x2a    schedule 25.03.2019
comment
Хороший прорыв, спасибо! Я согласен с неуклюжим видом строкового литерала (который, по мнению PEP, даже не является элегантным). Использование синтаксиса # type: ... может быть удобным, поскольку библиотека, с которой я работаю, должна хорошо взаимодействовать со старым программным обеспечением. - person pasta_sauce; 25.03.2019
comment
Единственная часть шрифта, намекающая на то, что я пытаюсь понять, — это цель. Отличным примером является сравнение строкового литерала и прямой ссылки на имя класса. Поскольку Python не заботится о том, как он хранится, и не несет никакой ответственности за его разрешение (как показано в моем обновлении), очевидно, что основная цель подсказок типов — для сторонних инструментов, таких как mypy или другие статические анализаторы. Это правильно? - person pasta_sauce; 25.03.2019
comment
@pasta_sauce - да, подсказки типов предназначены почти исключительно для использования сторонними инструментами. Есть некоторые библиотеки (например, force, typeguard), которые пытаются использовать подсказки типов во время выполнения, но они не очень широко используются. - person Michael0x2a; 26.03.2019
comment
Только что наткнулся на третье решение в PEP 563; это спасло мой день! - person Aubergine; 10.07.2020
comment
Для тех, кто интересуется, когда поведение станет поведением по умолчанию, главный ответ в ask.xiaolee.net/questions/1033177 говорит, что он станет стандартным в python 4.0 - person Almenon; 30.09.2020