Предыстория
PEP 3107
Одним из нововведений Python 3.0 было введение нового синтаксиса, позволяющего сохранять произвольные метаданные аргументов и возвращаемого значения функций в виде аннотаций. Нововведение было описано в PEP 3107.
Согласно ему аннотации функций:
совершенно необязательны к использованию;
способ связать произвольные выражения Python с различными частями функции во время ее определения интерпретатором.
Для Python аннотации не представляют никакого интереса и никак им не используются. Он просто делает аннотации доступными для сторонних инструментов.
Эти инструменты могут использовать их как им будет угодно. К примеру, для добавления к аргументам функции дополнительного описания:
def func(arg: "arg description"):
pass
Или для обеспечения проверки типов аргументов и возвращаемого значения функций:
def func(arg: int) -> bool:
pass
В PEP 3107 подчеркивалось, что сами по себе, описанные выше примеры, бессмысленны. Они приобретают смысл лишь для сторонних инструментов, таких как статические анализаторы и т.д.
Доступ к аннотациям можно получить с помощью специального словаря __annotations__
:
def func(arg: "arg description"):
pass
print("Annotations: ", func.__annotations__)
Запустим скрипт:
$ python ./example.py
Annotations: {"arg": "arg description"}
PEP 484 - Подсказки типов
В PEP 3107 сознательно не была определена семантика аннотаций функций и способ их использования.
Однако с течением времени стало ясно, что большинство сторонних инструментов используют аннотации функций в качестве подсказок типов аргументов и возвращаемого значения функций. И команда разработчиков Python решила стандартизировать определение и использование аннотаций в качестве подсказок типов.
Так в Python 3.5 был представлен PEP 484, который включал в себя описание синтаксиса и способов использования подсказок типов. А также описание ситуаций, в которых подсказки типов не должны быть использованы или необходимо использовать специальный синтаксис их определения.
Описанное в PEP 484 было реализовано в модуле typing
, который был добавлен в стандартную библиотеку.
В основном, команда разработки видела использование подсказок типов для статического анализа кода (статический анализатор кода mypy послужил вдохновением на написание PEP 484), хотя подсказки типов были доступны и в период выполнения программы, через атрибут __annotations__
. Но проверка типов в период выполнения программы серьезно не рассматривалась.
Также стоит отметить предварительный статус этого PEP. Это означает, что все, в будущем добавленные изменения в модуль typing
, должны быть добавлены и в предыдущие версии языка Python. К примеру, в Python 3.6 валидной стала следующая конструкция Dict[str, Tuple[S, T]]
, хотя в Python 3.5 это было не так. Это и прочие улучшения добавленные с выходом Python 3.6, так же были обратно-портированы в Python 3.5.
Опережающие ссылки
Подсказка типа указывающая на объект, который еще не был определен, называется опережающей ссылкой (forward reference).
class Tree:
def __init__(self, height: int, children: List[Tree]):
self.height = height
self.children = children
Запуск приведенного выше кода, закончится неудачей. Подсказки типов оцениваются в момент определения функций, методов или модулей. Так как Tree
, на момент оценки подсказок типов еще не определен, при их оценке интерпретатор возбудит исключение NameError
с ошибкой name 'Tree' is not defined
.
Получается, что при использовании подсказок типа, для вспомогательных целей, не относящихся к основной работе кода, в него был добавлен баг. В PEP 484 опережающие ссылки предлагалось определять как обычные литералы строк:
class Tree:
def __init__(self, height: int, children: List['Tree']):
self.height = height
self.children = children
Позже, при статическом анализе кода, инструменты должны были воспользоваться eval
для преобразования литералов строки в объект Python.
В связи с этим, на подсказки типов определенные таким образом, накладывались следующие требования:
строковый литерал должен содержать синтаксически правильное выражение Python;
он должен оцениваться интерпретатором без ошибок, как только модуль будет полностью загружен;
локальное и глобальное пространство имен, в которых происходит их оценка, должны совпадать.
Такое решение имело ряд недостатков: приходилось запоминать случаи, когда следовало использовать литералы строк, вместо ссылки на класс, да и сам синтаксис определения выглядел странно.
PEP 563 - Отложенная оценка аннотаций
PEP 563 решал следующие проблемы:
проблему опережающих ссылок (странный синтаксис их определения);
подсказки типов обрабатывались в момент определения функций, методов и модулей, что могло быть вычислительно затратно и увеличивало время запуска программы.
Теперь разработчик мог определить подсказки типов следующим образом:
class Tree:
def __init__(self, height: int, children: List[Tree]):
self.height = height
self.children = children
Достигалось это за счет изменения способа оценки подсказок типов, после введения данного PEP все подсказки типов представлялись как литералы строк. То есть, для интерпретатора приведенный выше код выглядел бы так:
class Tree:
def __init__(self, height: "int", children: "List[Tree]"):
self.height = height
self.children = children
Такие подсказки типов никак не оценивались, а лишь записывались в __annotations__
, что снижало затраты вычислительных ресурсов.
Статические анализаторы кода, не должны были испытывать особых проблем при анализе таких подсказок типа, так как уже умели их оценивать.
Для оценки подсказок типа в период выполнения, пользовательский код должен выполнить их преобразование из строки в объект Python, при помощи новой функции typing.get_type_hints
.
Такой способ оценки аннотаций накладывал на них следующие требования:
строковый литерал должен содержать синтаксически правильное выражение Python;
для оценки должны быть использованы аннотации только с именами, представленными в пространстве имен модуля, поскольку отложенная оценка локальных пространств имен не надежна.
Данный PEP был добавлен вместе Python 3.7, однако его необходимо активировать вручную, путем импорта from __future__ import annotations
. Описанный способ обработки аннотаций должен был стать способом по умолчанию с выходом Python 3.10.
Скрытая угроза
В то время, когда велось обсуждение PEP 563, подсказки типов в основном рассматривались в контексте статического анализа кода. Использование их в период выполнения всерьез не рассматривалось и особо не обсуждалось. Как было сказано выше, разработчики оставили возможность конвертации строк обратно в объекты Python, при помощи typing.get_type_hints
.
Однако с течением времени стало ясно, что предложенный способ оценки аннотаций хорошо подходит для статического анализа кода, но совсем не годится для использования аннотаций в период выполнения программы. Оказалось, что typing.get_type_hints
имеет ограничения, которые делают его использование дорогостоящим во время выполнения программы и, что более важно, недостаточным для оценки всех типов.
Самый распространенный пример касается не глобального пространства имен, в котором генерируются типы (например, внутренние классы, классы внутри функций и т.д.). Но один из ярких примеров опрежающих ссылок: классы с методами, принимающими или возвращающими объекты своего собственного типа, также не обрабатываются должным образом с помощью typing.get_type_hints
.
В преддверии "заморозки" Python 3.10 автор Pydantic создал issue на Github, где заявил, что Pydantic возможно никогда не сможет адаптировать новый способ оценки аннотаций для использования их в период выполнения программы. Также была другая проблема, так как отложенная оценка аннотаций станет способом по умолчанию, без возможности перехода на старый способ, это сломает большую часть Pydantic и нарушит работу FastAPI.
Он призвал сообщество разработчиков, использующих его библиотеку, обратиться к Управляющему совету Python с просьбой отложить введение отложенной оценки и разработать новый способ оценки аннотаций. Благо такой способ уже был предложен в PEP 649.
В течение нескольких дней после публикации issue, Управляющий совет Python связался с авторами библиотек Pydantic и FastAPI для обсуждения сложившейся ситуации. Как оказалось основные разработчики даже не слышали про эти библиотеки и были удивлены, что кто-то использует аннотации в период выполнения программы.
После долгих обсуждений Управляющий совет Python объявил, что откладывает введение отложенной оценки аннотаций с использованием строк до Python 3.11. Тем самым давая возможность разработчикам решить проблемы обратной совместимости. Однако они также отложили рассмотрение PEP 649 в качестве замены для отложенной оценки с использованием строк до Python 3.11.
PEP 649 - Отложенная оценка аннотаций с использованием дескрипторов
В Python 3.9 доступно два способа оценки аннотаций типов:
изначальный способ, оценивающий аннотации во время определения функции (PEP 484);
отложенная оценка аннотаций (PEP 563).
При использовании изначального способа, существовала проблема связанная с циклическими ссылками. Отложенная оценка аннотаций решала эту проблему, но добавляла новые, например, невозможность оценки аннотаций находящихся в локальных пространствах имен.
PEP 649 предлагает третий способ оценки аннотаций, объединяющий в себе лучшие стороны имеющихся способов. Он решает проблему циклических ссылок, оставляя семантику оценки аннотаций такой же как и в изначальном способе, то есть позволяя проводить оценку аннотаций локальных и классовых пространств имен.
В новом подходе, код для создания словаря аннотаций, записывается в функцию. При этом, __annotations__
является обычным дескриптором данных, который вызывает функцию вычисления словаря аннотаций и сохраняет результат ее выполнения. Такая реализация откладывает оценку аннотаций до обращения к дескриптору __annotations__
, к этому моменту все проблемы, связанные с циклическими ссылками, будут уже решены. И если к __annotations__
не будет вообще обращений, то и код для оценки аннотаций не будет исполнен, что снижает вычислительные затраты.
Аннотации, определенные таким способом, имеют такую же область видимости, что и изначальный способ. К тому же такие аннотации могут ссылаться на имена определенные после определения самих аннотаций. Единственным ограничением является то, что все имена используемые в аннотациях, должны быть определены до вызова дескриптора __annotations__
.
Если предложенный способ будет принят, то он полностью заменит семантику PEP 563.
Рассмотрим следующий пример:
def foo(x: int = 3, y: MyType = None) -> float:
...
class MyType:
...
foo_y_type = foo.__annotations__['y']
Аннотации доступны в период выполнения программы через атрибут __annotations__
функции, класса или модуля. Когда аннотации определены для одного из этих объектов, __annotations__
это отображение имен аргументов на значение определенные в аннотациях к этим аргументам.
В Python 3.9 по умолчанию оценка аннотаций происходит в момент определения функции, класса или модуля, результаты оценки записываются в словарь. В период выполнения программы, описанное выше, примерно выглядит так:
annotations = {'x': int, 'y': MyType, 'return': float}
def foo(x = 3, y = "abc"):
...
foo.__annotations__ = annotations
class MyType:
...
foo_y_type = foo.__annotations__['y']
Этот код не выполнятся, так как содержит опережающие ссылки. Код, использующий отложенную оценку аннотаций, предложенную в PEP 563, приведен ниже:
annotations = {'x': 'int', 'y': 'MyType', 'return': 'float'}
def foo(x = 3, y = "abc"):
...
foo.__annotations__ = annotations
class MyType:
...
foo_y_type = foo.__annotations__['y']
Проблемы, связанные с такой оценкой, описаны выше. Пример оценки аннотаций новым способом приведен ниже:
class function:
# атрибут __annotations__ функции это уже "дескриптор
# данных", просто мы изменяем его поведение.
@property
def __annotations__(self):
return self.__co_annotations__()
# ...
def foo_annotations_fn():
return {'x': int, 'y': MyType, 'return': float}
def foo(x = 3, y = "abc"):
...
foo.co_annotations = foo_annotations_fn
class MyType:
...
foo_y_type = foo.__annotations__['y']
Важным изменением является то, что код, создающий словарь аннотаций, теперь находится в функции, называемой здесь foo_annotations_fn()
. Но эта функция не вызывается, пока мы не запросим значение foo.__annotations__
, и мы не делаем этого до тех пор, пока не будет определен MyType
. Таким образом, этот код также выполняется успешно, и теперь foo_y_type
имеет правильное значение - класс MyType
- даже несмотря на то, что MyType
не был определен до тех пор, пока не была определена аннотация.
Итоги
На текущий момент проблема опережающих ссылок остается до конца не решенной. Способ решения, подходящий для статического анализа кода, совершенно не подходит для использования в период выполнения. Сообщество разработчиков использующих Pydantic и FastAPI продолжает расти (FastAPI занимает третье место среди веб-фремворков Python) и разработчикам языка Python придется учитывать их мнение в вопросах развития аннотаций.
Несмотря на некоторую неопределенность, думаю, что в будущем нас ждет еще больше способов использования аннотаций в период выполнения программы. Спасибо за прочтение!
Teplo_Kota
Сначала создали язык, где за счёт очень вольной трактовки типов и двух тонн синтаксического сахара можно кодить очень быстро. Теперь это всё уже застыло в камне, и поверх этого начинают добавлять отсутствие — да, именно добавлять отсутствие вольностей, чтобы получить скорость. Получается фигня.
Tanner
Во-первых, ни о какой скорости речи не идёт, это не те аннотации, которые в numba. Во-вторых, добавлять опциональное отсутствие, раз уж на то пошло.
dravor
Да тут вообще непонятно о чем именно речь идет: чем больше вдумываешься, тем менее очевидно. Питон сделали именно как компактный с наглядным легко читаемым синтаксисом (скоуп видимости через очень спорное форматирование отступами ради наглядности).
Вообще почти все решения продвигались создателем языка именно ради простоты и наглядности. За что он и был с успехом воспринят как в академ среде, так и в смежных отраслях, двигающих софтовую часть IT.
А теперь выясняется, что все нужно бросить и начать переживать: «как там бедные статические анализаторы? Не тратят ли лишние такты CPU?»
После прочтения статьи остро вспомнились последние стандарты плюсов — из лаконичного языка он превратился в что-то трудно удержимое в голове без справочника под рукой.
Tanner
Речь о том (по крайней мере, для меня), что, начиная с версии 2.6, в Python появилась возможность ассоциировать с любой переменной или возвращаемым значением функции некий кусочек метаданных. Всё остальное отдано на усмотрение пользователей языка. Модуль
typing
— это просто пример того, как эти метаданные могут быть использованы.voro6yov Автор
Далеко не все используют аннотации, многие про них и не слышали. Для таких людей Python в плане синтаксиса, каким был таким и остался.
Аннотации же вещь чисто опциональная, хочешь используешь, хочешь нет. К тому же, проблема дополнительных вычислительных затрат относиться лишь к моменту запуска программы, в период выполнения, аннотации никак вычислительные ресурсы не используют.
dimaaannn
Питон остался всё таким же компактным.
Однако сильно упростился для написания кода, когда тебе не нужно проверять что именно возвращает та или иная функция, или делать всякие шаманские телодвижения чтобы ИДЕ показала подсказки типизации.
katletmedown
При чем тут «бедные статические анализаторы»? Глазами читать типизированный код сильно проще.
menstenebris
Как же не идёт, когда сам Гвидо пилит mypy в комплекте с mypyc который как раз опираясь на статическую типизацию компилирует код на питоне, с уточнением структур и типов, в код на си. Например инт-объект превращается в инт на си, занимает меньше памяти и работает быстрее. На их собственных бенчмарках производительно вырастает почти в 10 раз, на моих личных тестах всего в 5, что тоже неплохо.
0xd34df00d
Думаю, что человек выше писал не о скорости выполнения, а о скорости кодинга.
event1
Да, всё так. Потому что, Гвидо делал язык, чтоб быстро скрипты до 100-и строчек на коленке делать, а потом на нём зачем-то стали ваять всякие фреймворки и приложения с десятками участников и тысячами строк кода. В таких приложениях, конечно нужна типизация. И, конечно, важна производительность. Пока старый синтаксис не запретили, можно ещё пользоваться. Потом придётся переходить на Lua.