- Аннотация
- Мотивация
- Обоснование Обзор Реализация Влияние на производительность Использование вне аннотаций
- Обзор
- Реализация
- Влияние на производительность
- Использование вне аннотаций
- Спецификация Формат AST Вспомогательные функции
- Формат AST
- Вспомогательные функции
- Обратная совместимость
- Вопросы безопасности
- Как этому обучать
- Эталонная реализация
- Отклоненные идеи Создание объектов типов внутри функций аннотирования Хранение исходного кода аннотаций Невозврат пространств имен AST
- Создание объектов типов внутри функций аннотирования
- Хранение исходного кода аннотаций
- Невозврат пространств имен AST
- Авторское право
Аннотация
В настоящее время интерпретатор Python позволяет использовать любое выражение в качестве аннотации. Однако их наиболее популярное применение — аннотации типов — ограничено лишь небольшой подгруппой возможных выражений. Это связано с тем, что аннотации типов должны быть доступны для интроспекции во время выполнения, а многие допустимые выражения создают значения, для которых это невозможно. Это сильно ограничивает набор выражений, которым можно придать смысл в рамках спецификации типизации, а также ограничивает выразительность и удобство использования аннотаций типов.
Данный PEP предлагает расширить функции аннотирования таким образом, чтобы в аннотациях типов можно было использовать любое выражение Python. Это достигается путем создания нового формата аннотаций, который предписывает функциям аннотирования возвращать AST аннотации и пространства имен. Потребители аннотаций типов во время выполнения могут затем вычислять их в объекты типов.
Мотивация
Аннотации типов в Python — это выражения, привязанные к именам переменных, которые обозначают, какие значения могут быть присвоены переменной. Они доступны не только линтерам и средствам проверки типов непосредственно через исходный код, но и для использования в рефлексии во время выполнения, поскольку интерпретатор создает специальные функции аннотирования, которые вычисляют объекты, в которые превращаются выражения аннотаций.
Хотя этот подход хорошо работает для многих существующих аннотаций и прост для понимания, он также сильно ограничивает виды выражений, которые могут быть использованы в качестве аннотаций типов. В абстрактном смысле это происходит потому, что функции аннотирования вычисляют такие выражения, используя тот же механизм, который Python использует для обычных выражений значений, и во многих случаях эта семантика несовместима с потребностями рефлексии во время выполнения.
Теперь мы приведем несколько более конкретных примеров выражений, которые могли бы найти применение в аннотациях типов, но в настоящее время невозможны. Однако мы не выступаем напрямую за какое-либо конкретное использование, и данный PEP не реализует ни одно из них. Скорее, мы закладываем основу, которая сделает их и подобные будущие предложения возможными.
PEP 586 представил литеральные типы, которые перечисляют конкретный список возможных литеральных значений, например, числа 1, 2 и 3. В настоящее время это записывается как Literal[1, 2, 3]. Многие пользователи интуитивно хотят записывать это как 1 | 2 | 3, выписывая голые литералы и используя оператор объединения для их соединения, что также используется в других языках, таких как TypeScript. В настоящее время это невозможно в Python, потому что выражение 1 | 2 | 3 вычисляется просто как 3, поскольку | интерпретируется как операция побитового ИЛИ, а не как объединение типов. Более того, свертка констант исключает появление этого выражения в сгенерированном байт-коде, и поэтому невозможно получить фактическую аннотацию типа во время выполнения.
Приведенный выше пример лишь мешает нам использовать немного более короткие записи для уже существующих типов. Существует также множество аннотаций типов, которые либо вполне возможны, либо даже обсуждаются в настоящее время, но которые невозможно реализовать в текущих условиях. Текущий открытый черновик PEP 827 вводит множество таких типов. Например, он предлагает условные типы — это выражения типов, которые вычисляются в один из двух типов в зависимости от некоторого условия. Интуитивно их можно было бы записать как FirstType if TypeCondition else OtherType, подобно тернарным выражениям. Но это невозможно, потому что независимо от того, во что вычисляется TypeCondition во время выполнения, одно из других выражений типов никогда не будет вычислено и, следовательно, будет невидимым для интроспекции во время выполнения.
Подобные проблемы возникают при определении других типов, которые определяются с использованием других существующих типов. Например, можно было бы написать {K: NotRequired[T] for K, T in SomeTypedDict}, чтобы определить типизированный словарь, который имеет то же определение, что и существующий типизированный словарь, но где каждый ключ является необязательным. В настоящее время это невозможно, так как объекты типов нельзя перебирать таким образом. Это также уже обсуждалось, например, в этой ветке.
Обоснование
Обзор
Мы предлагаем сделать возможным использование произвольных выражений в выражениях типов, сохраняя при этом полные возможности интроспекции во время выполнения, путем введения нового формата для функций аннотирования. Он будет предписывать им возвращать объекты, содержащие как AST аннотаций, так и их пространство имен. Затем их можно использовать для создания фактических объектов типов, которые интересуют пользователей интроспекции во время выполнения.
Например, рассмотрим следующий класс:
При вызове MyClass.__annotate__(Format.VALUE) он по-прежнему будет возвращать обычный словарь аннотаций {"a": int, "b": list[str]}. Но при вызове как MyClass.__annotate__(Format.AST) мы получим этот словарь:
Однако большинство пользователей никогда не увидят эти объекты напрямую. Вместо этого мы предлагаем добавить новую функцию get_type_annotations в модуль typing, которая будет внутренне выполнять вышеуказанный вызов, а затем возвращать привычный словарь аннотаций {"a": int, "b": list[str]}.
Сила этого подхода заключается в том, что он позволяет нам реализовывать новые выражения типов, такие как упомянутые выше, просто расширяя логику вычисления в get_type_annotations. Эта логика может быть полностью отделена от обычной семантики выражений Python и вместо этого создавать объекты типов, которые позволяют полную интроспекцию во время выполнения.
Мы также предлагаем добавить некоторые дополнительные вспомогательные функции, связанные с функциями аннотирования и этими объектами AST. В частности, новую функцию create_annoate_function в модуле annotationlib для легкого синтеза функции аннотирования. Основная часть этой функциональности уже реализована в модуле dataclasses, и мы предвидим, что многим пользователям это понадобится для создания функций аннотирования, поддерживающих этот несколько более сложный формат.
Реализация
Хотя точный механизм, используемый функциями аннотирования, является деталью реализации, может быть полезно взглянуть на то, как это может выглядеть. Рассмотрим, например, класс из примера выше:
В рамках этого предложения его функция аннотирования может вести себя аналогично этому коду на Python:
Это работает по сути в три этапа. Сначала заполняется словарь пространства имен путем выполнения обычного поиска имен переменных. Это важно для того, чтобы мы могли позже вычислить возвращаемые объекты AST, используя правильные привязки для каждой переменной. Затем загружаются некоторые строковые константы, содержащие двоичные данные, определяющие AST каждой аннотации. Это компактный формат, который легко реализуется с помощью существующей функциональности компилятора. Кроме того, его парсинг происходит гораздо быстрее, чем хранение исходного кода аннотаций напрямую. Затем функция annotate использует новую внутреннюю возможность для фактического построения необходимых объектов AST аннотаций.
Предлагаемая функция get_type_annotations будет вычислять объекты типов, используя функцию, подобную этой:
Влияние на производительность
Хотя каждую новую функцию необходимо оценивать с точки зрения ее влияния на производительность и сложность, функции, связанные с типизацией, заслуживают дополнительного внимания, поскольку аннотации типов являются полностью необязательной частью языка Python. Таким образом, нам нужно рассмотреть три отдельные группы пользователей и влияние на них: пользователи, которые вообще не используют аннотации типов; пользователи, которые аннотируют свой код для инструментов проверки типов и/или линтеров, но не используют интроспекцию во время выполнения; и, наконец, пользователи, которые также вычисляют свои аннотации типов во время выполнения.
Для этого мы рассмотрели три метрики: время, необходимое для импорта модулей как без аннотаций типов, так и с ними; объем памяти, занимаемый функциями annotate; и время, необходимое для фактического вычисления функций annotate. Время импорта наиболее важно для первой группы пользователей; на вторую группу также влияет объем памяти функций annotate, а время вычисления функций annotate влияет только на последнюю группу пользователей.
Используя нашу эталонную реализацию, мы не обнаружили существенной разницы во времени импорта модулей, которые не используют аннотации типов. Для модулей, которые используют аннотации, импорт был умеренно быстрее, а объем памяти — немного меньше при использовании предлагаемых функций annotate, в обоих случаях на несколько процентов. Но, к сожалению, время вычисления может значительно увеличиться. В худшем случае, когда аннотации запрашиваются в формате значения и каждое используемое имя определено, увеличение составляет примерно семикратный размер. Однако, когда некоторые имена не определены и приходится использовать форматы STRING или FORWARDREF, текущий подход также значительно медленнее и приводит к результатам, сопоставимым с предлагаемыми функциями annotate.
Распространенная ситуация, когда аннотации типов вычисляются, возникает, когда такие инструменты, как dataclasses или аналогичные пакеты ORM, анализируют определения классов или функций для синтеза дополнительного поведения. Для этих инструментов время, например, создания dataclass будет затронуто этим предложением. Но, как упоминалось выше, уже существует много ситуаций, когда проверка аннотаций занимает аналогичное количество времени.
В целом, поскольку негативное влияние на производительность затрагивает только наименьшую группу пользователей, которые также получают выгоду от новых возможностей аннотаций типов, мы считаем это оправданным компромиссом. Спецификация предлагаемого формата также достаточно открыта, чтобы можно было реализовать множество оптимизаций, если они будут сочтены необходимыми в будущем. Для многих пользователей это предложение даже даст небольшое повышение производительности, поскольку их код никогда не вычисляет какие-либо функции annotate.
Использование вне аннотаций
Хотя это предложение позволяет использовать будущие выражения типов в аннотациях, существуют и другие места, где пользователи хотят писать выражения типов. Например, в cast(<некоторое сложное выражение>, value). Поскольку интерпретатор не может отличить эти случаи от других вызовов функций, он не может сделать вывод, что должен использовать механизм, подобный тому, который мы предлагаем.
Эту проблему можно избежать, используя промежуточный псевдоним типа:
Это позволяет использовать любое новое выражение типа внутри <некоторого сложного выражения>, поскольку псевдонимы типов также реализованы с использованием функций annotate.
Хотя это решение представляет собой лишь обходной путь для данной проблемы, более полное исправление потребовало бы добавления нового ключевого слова в язык Python, что, по нашему мнению, не является необходимым. Это решение можно будет пересмотреть в будущем, если использование подобных промежуточных псевдонимов типов действительно создаст серьезную проблему в реальном коде.
Спецификация
На протяжении всего этого PEP, когда мы говорим о функциях/методах annotate, мы имеем в виду как специальные методы __annotate__, встречающиеся у некоторых объектов, так и следующие методы, встречающиеся у объектов typing:
- evaluate_value в typing.TypeAliasType
- evaluate_bound, evaluate_constraints и evaluate_default в typing.TypeVar
- evaluate_default в typing.ParamSpec
- evaluate_default в typing.TypeVarTuple
Когда мы ссылаемся на их возвращаемые значения, мы имеем в виду либо объекты, содержащиеся в словаре, возвращаемом методами __annotate__, либо единственный объект, возвращаемый другими методами.
Формат AST
Новое значение под названием AST добавляется в перечисление annotationlib.Format со значением 5. Функции annotate не обязаны поддерживать этот формат. Если функция annotate вызывается с этим форматом, она должна вернуть объект AnnotationAST. Это экземпляры предлагаемого нового класса, которые хранят AST аннотации в виде объекта ast.expr и пространство имен, используемое для их вычисления.
Функции annotate, созданные компилятором, всегда будут поддерживать этот формат. Они будут хранить необходимые данные AST в виде строковых констант, а затем каждый раз при вызове создавать новые объекты AnnotationAST. Содержащиеся объекты AST будут идентичны объектам, созданным путем прямого парсинга исходного кода аннотации.
Вспомогательные функции
Добавляется новая функция typing.get_type_annotations, которая работает аналогично существующей annotationlib.get_annotations, но вместо этого вызывает базовую функцию annotate с Format.AST, а затем конструирует объекты typing, используя новый вспомогательный метод typing.evaluate_type_ast.
Существующая функция typing.get_type_hints будет объявлена устаревшей. Она имеет немного другую семантику по сравнению как с annotationlib.get_annotations, так и с предлагаемой функцией, что делает невозможным ее изменение для поддержки новой функциональности. Она также станет полностью излишней, поскольку пользователям аннотаций типов нужно будет вызывать get_type_annotations, чтобы правильно разрешить любые аннотации типов, содержащие новые функции типизации. Оставление этой функции в текущем виде только создаст путаницу относительно того, какую функцию следует использовать.
Формат AST также предоставляет более простой и надежный метод создания аннотаций в форматах STRING и FORWARDREF. В настоящее время функции annotate, созданные компилятором, не поддерживают их напрямую; скорее, вспомогательные функции в annotationlib пытаются создать AST с максимальными усилиями, а затем депарсят его в запрошенный формат. Согласно этому PEP, эти вспомогательные функции будут вместо этого использовать формат AST и депарсить результат. Во многих случаях это дает более точные результаты для этих форматов.
Чтобы упростить создание синтезированных функций аннотирования, в модуль annotationlib будет добавлена новая вспомогательная функция annotationlib.create_annotate_function. Она принимает отображение имен переменных на объекты аннотаций в одном из существующих форматов аннотаций. Используя их, она возвращает функцию аннотирования, которая поддерживает все форматы аннотаций путем соответствующего преобразования переданных значений.
Обратная совместимость
Новый формат аннотаций и вспомогательные функции добавляют только новые возможности, поэтому проблем с обратной совместимостью не возникает. Устаревание typing.get_type_hints означает, что существующий код, использующий эту функцию, перестанет работать после ее удаления из стандартной библиотеки. Мы рекомендуем пользователям перейти на annotationlib.get_annotations или typing.get_type_annotations, в зависимости от того, какую семантику они хотят использовать. Хотя в некоторых случаях эти функции ведут себя немного иначе, в большинстве ситуаций они являются прямой заменой.
Мы также хотим отметить, что будущие дополнения, подобные тем, что описаны в разделе «Мотивация», не создают проблем с обратной совместимостью, даже если их внедрять постепенно.
Рассмотрим, например, изменение в спецификации typing, при котором var: 1 интерпретируется как литеральный тип Literal[1]. Пользователи, которые вычисляют метод аннотирования напрямую или с помощью одного из существующих вспомогательных методов, по-прежнему будут видеть результат как обычное целое число 1. Измененная семантика учитывается только тогда, когда пользователь сам выбирает ее, вызывая функцию аннотирования с Format.AST или используя typing.get_type_annotations.
В наиболее распространенном случае использования аннотации потребляются не пользователем напрямую, а библиотечным кодом, который проводит интроспекцию пользовательских объектов. Таким образом, принятие новой семантики аннотаций может быть затруднено, если авторы библиотек будут беспокоиться о том, что существующие пользовательские аннотации будут переинтерпретированы в другие объекты при обновлении библиотеки. Но это также не является проблемой. Спецификация typing уже учитывает вопросы обратной совместимости, что означает, что любые текущие допустимые аннотации типов не будут изменены так, чтобы означать что-то другое. Потенциальные будущие изменения могут определять новую семантику только для синтаксических конструкций, которые в настоящее время не являются допустимыми аннотациями типов и, следовательно, не встречаются в пользовательском коде.
Последствия для безопасности
Для данного изменения нет известных последствий для безопасности. Вызов функций аннотирования уже мог приводить к выполнению произвольного кода, определенного в аннотируемом объекте. Python также уже предоставляет возможность доступа к исходному коду и скомпилированному байт-коду интроспектируемых объектов. Таким образом, объекты AST, возвращаемые в новом формате, не содержат никакой ранее недоступной информации.
Как этому обучать
Пользователи аннотаций не затрагиваются этим предложением напрямую. Оно призвано упростить способ написания аннотаций типов; любые будущие изменения, основанные на этом PEP, должны будут оцениваться по их собственным достоинствам. Документация будет информировать пользователей о том, что любая новая семантика поддерживается нативно только в аннотациях. Поскольку это, безусловно, самое распространенное место для указания типов, мы ожидаем, что это не станет большим ограничением. В других местах, где могут встречаться типы, пользователи уже должны заключать прямые ссылки (forward references) в строковые литералы, так что это известная практика. Инструменты статического анализа типов (type checkers) должны предупреждать пользователей, если они не заключают форму типа в кавычки, используя синтаксис, который был бы неверно вычислен в месте, где статически известно, что ожидается форма типа.
Новый формат и изменения во вспомогательных функциях будут задокументированы как часть стандарта языка. Библиотеки, которые проводят интроспекцию аннотаций типов, смогут легко поддерживать любой новый синтаксис типов, вызывая предоставленные вспомогательные функции, и должны документировать это поведение, чтобы их пользователи были осведомлены о любых потенциальных будущих изменениях.
Одной из потенциальных проблем является «Использование вне аннотаций», обсуждавшееся в начале. Поскольку большинство пользователей аннотаций типов также используют инструменты статического анализа типов и/или линтеры, мы рекомендуем этим инструментам внедрить проверки на наличие таких ошибок и предлагать исправление через промежуточный псевдоним типа.
Эталонная реализация
Данное предложение реализовано в виде прототипа в форке CPython.
Отклоненные идеи
Создание объектов Typing внутри функций аннотирования
Первоначальная идея заключалась в том, чтобы изменить функции аннотирования так, чтобы они сами создавали соответствующие объекты типов. Этого можно достичь несколькими способами, например, с помощью нового необязательного аргумента для сигнализации семантики, специфичной для typing, или нового формата. Эти подходы были отклонены, поскольку они вынуждают определять семантику, специфичную для типов, непосредственно в интерпретаторе. Это не только ограничивает семантику (например, отсутствие необходимости поиска в пространстве имен), но и привязывает использование функций typing к используемой версии Python, вместо того чтобы позволить текущую возможность бэкпортинга через typing_extensions.
Хранение исходного кода аннотаций
Вместо хранения бинарных данных, определяющих AST аннотаций, альтернативой является простое хранение исходного кода аннотаций напрямую. Это также было представлено как возможность еще в PEP 649. Эти два подхода в значительной степени эквивалентны, поскольку можно создать AST из исходного кода и наоборот. Хотя нераспарсенный AST не обязательно является той же самой строкой, которая была в исходном коде, поскольку AST не сохраняет точное форматирование кода, он семантически эквивалентен и, в частности, не подвержен влиянию оптимизаций компилятора.
При сравнении производительности эти два представления данных также в значительной степени эквивалентны с точки зрения времени импорта и использования памяти. Однако парсинг исходного кода для последующего создания объектов типов значительно медленнее, чем работа с данными AST, примерно в 3 раза. Хранение исходного кода также сильно ограничивает будущие оптимизации, поскольку представление данных напрямую раскрывается как API.
Невозврат пространств имен AST
Чтобы правильно вычислить AST в правильные объекты типов, функция вычисления должна иметь доступ к пространству имен, в котором была определена аннотация. Изначально кажется, что это пространство имен можно восстановить из объекта функции аннотирования, поскольку они содержат используемые globals и cellvars. Однако этого недостаточно, так как пространства имен могут использовать более сложную логику поиска при использовании операторов global или name mangling.
Авторское право
Этот документ передан в общественное достояние или под лицензию CC0-1.0-Universal, в зависимости от того, что является более разрешительным.





