Что такое аннотации типов в Python?
Читая эту статью надеюсь, что вы знакомы с аннотациями в Python. Но все же в вкратце напомню. Они нужны для того, чтобы придать некой строгости нашему динамически типизированному языку.
Как работает в IDE на примере PyCharm
Уже на этапе разработки мы можем видеть, что используем методы не так, если их автор писал аннотации к коду.
В примере аргумент функции first ожидает числовое значение, а мы передаем строку. Благодаря современным редакторам кода мы можем избежать потенциальных ошибок.
А что под капотом?
В python аннотации типов читаются на этапе импорта и хранятся в атрибуте __annotations__
. Посмотрим, что находится в этом атрибуте у нашего метода custom_sum
:
В этом атрибуте хранится dict, ключу return которого соответствует аннотация возвращаемого типа, идущая после ->.
Для получения аннотаций typing предоставляет метод get_type_hints
.
Как видно, значениями в словаре являются реальные классы python.
Накладные расходы?
К сожалению, добавление типов в код на python не дает никакого прироста производительности. Лишь вызывает некоторые накладные расходы:
Импорт модулей занимает больше времени и памяти(так как нужно прочитать аннотации и записать их в атрибут __annotations__);
Ссылка на типы, которые еще не определены требует использования строковых обозначений, а не реальных типов.
С первым пунктом все понятно. Приведу пример, когда второй пункт может доставить нам сложности.
Можно видеть проблему “опережающей ссылки”. Наш класс в одном из методов хочет вернуть экземпляр самого себя, но не сможет этого сделать, поскольку объект класса еще не определен, пока Python не закончит вычисление его тела. В этом случае мы вынуждены записать возвращаемое значение в виде строки. Из-за такого поведения аннотации обрабатывались в момент определения функций и модулей, что было вычислительно затратно(программа должна понять, что эта строка означает).
Одобренный PEP 563 “Postponed Evaluation of Annotations” уменьшил время необходимое для обработки аннотаций типов во время выполнения. Аннотации типов больше не вычисляются в момент определения функции, вместо этого они сохраняются в аннотациях в строковой форме(не производя никаких вычислений). Чтобы достичь этого нужно сделать один import.
Благодаря импорту from __future__ import annotations
можно увидеть, что типы стали простыми строками, хотя не определены, как типы в кавычках. Изначально, планировалось сделать такое поведение поведением по умолчанию(не импортируя ничего). Но разработчики FastAPI и pydantic настояли на том, чтобы эта часть была опциональной так как она ломает работу этих библиотек.
MyPy
MyPy нужен для статической проверки типов в Python. Эта библиотека проверяет наши аннотации и как мы ими пользуемся. Приведу совсем небольшой пример, подробнее можно ознакомится по ссылке:
При вызове функции test_func мы ошибочно передаем вместо str тип int. MyPy помогает нам понять, что мы что-то делаем не так:
Интересные примеры аннотаций
Я не буду рассказывать про базовые типы, по типу str, int и тд. Так же опущу распространенное типы из библиотеки typing по типу List, Union, Optional так как об этом очень много информации.
Аннотирование *args **kwargs
Можно установить какие типы должен принимать *args **kwargs. В коде ниже указано, что args принимает только строковые значения, а kwargs только число.
MyPy выдаст следующую ошибку:
Суть ошибки в том, что все неименованные аргументы *args ждут тип str, мы же передаем int.
Объект является подтипом
Если даны 2 класса User и ProUser, который наследует User. В таком случае мы можем передавать методу, ожидающему User тип ProUser.
Вызываемые аргументы
Так же мы можем аннотировать аргумент функции, даже если он является другой функцией, благодаря Callable из модуля typing. С помощью этого инструмента можно так же указать, что ждет функция и что она возвращает. В нашем случае метод bar принимает str и возвращает None.
Аннотирование переменных
До этого момента мы говорили лишь об аннотациях аргументов функций и возвращаемых значений. Помимо этого, аннотировать можно еще переменные.
Если говорить о себе - я не использую аннотации переменных. Так как типизация функций помогает нам работать с интерфейсами, тогда как работа с переменными только увеличивает время разработки. Если есть большое желание указывать типы везде - стоит присмотреться к другому языку программирования.
Типизация для декораторов
Зачастую аннотировать декораторы - не самая хорошая идея. Они не только не улучшают чтение кода, а делают более непонятным происходящее. Приведу пример найденного аннотирования декоратора на просторах интернета:
Так что в итоге?
К сожалению, в Python типизация не дает никакого прироста производительности, а только потенциальное замедление и увеличение потребления памяти. Так же, чтобы корректно ее использовать - нужно потратить определенное время, чтобы разобраться что к чему.
Если говорить о философии языка - он не про это, python больше про скорость разработки и простоту использования. Аннотация раскрывает себя во всей красе на больших проектах, когда нам важна общая надежность системы. Такие инструменты как MyPy подскажут нам на то, что мы делаем не так. Говоря о небольших проектах “на скорую руку” или скриптах - я бы не стал использовать типизацию.
Из интересного можно подметить, что далеко не все core разработчики самого Python используют Type hints в своем коде.
Сам я уже не могу не использовать аннотации, для меня это вошло в привычку, для меня это как правило хорошего тона в разработке. Но своих коллег по работе принуждать к типам я не стану, а скорее просто делюсь преимуществами их использования.
Дорогой ценой я выучил урок: для небольших программ динамическая типизация - благо. Но для более крупных программ необходим более дисциплинированный подход.
Гвидо Ван Россум, фанат Монти Пайтон
Полезные источники
Ромальо Л. PYTHON. К вершинам мастерства / 2-е издание / Москва: ДМК пресс, 2022. - 897
MyPy documentation - URL: https://mypy.readthedocs.io/en/stable/
PEP 484 – Type Hints - URL: https://peps.python.org/pep-0484/
PEP 483 – The Theory of Type Hints - URL: https://peps.python.org/pep-0483/
typing — Support for type hints - URL: https://docs.python.org/3/library/typing.html
Комментарии (58)
ednersky
15.03.2024 06:57Ох.
Всё это "типовое" поветрие давно (ЕМНИП в 1993м году) уже под лупой исследовано Ларри Уоллом.
Когда Ларри разбирал призыв: "переходите на типы", то он наткнулся, что кроме вот этого мема никто ничего в аргументы не приводит:
Комментарий для зануд (конечно, Ларри рассматривал современный ему мем, речь всё-таки о 1993 годе).
Так вот, увидев, что НИКАКИХ других обоснований к призывам использовать типы у адептов типов нет, Ларри рассмотрел примеры из мема и сделал вывод:
проблема здесь состоит не в типах, а в неоднозначном применении оператора "плюс". В одном случае он означает "математическое сложение", а в другом - "конкатенация".
Далее, Ларри предложил в новых языках программирования разделять эти операторы, выделяя их в разные значки, и многие языки стали следовать этому правилу. И в них неоднозначности из мема нет. И необходимости в типах тоже нет.
И нагрузки на программиста "размечай свой код, чтобы сделать компилятор счастливым" - тоже нет.
fireSparrow
15.03.2024 06:57+6Речь-то в статье вообще про другое. В питоне и так типы есть, сложить строку с числом у вас не получится. Проблема в том, что тип становится известен только в процессе выполнения.
Поддерживать код, у которого не размечены типы - очень-очень больно.
Сейчас я работаю на проекте, где в одном из сервисов есть огромное число функций, которые принимают аргумент opts, или params, или оба этих аргумента одновременно. Эти аргументы являются словарями, которые формируются постепенно - то есть по мере того, как они пробрасываются из функции в функцию, они могут обогащаться новыми ключами.
В итоге глядя только на функцию, вы не можете знать, какие ключи вообще есть в этих словарях, и какой формат могут иметь их значения. Приходится каждый раз долго лазить по всему коду, и всё равно полной уверенности нет.
А если бы в эти аргументы передавались датаклассы, и были бы проставлены соответствующие аннотации, то это полностью избавило бы меня от такой проблемы.
ednersky
15.03.2024 06:57Речь-то в статье вообще про другое. В питоне и так типы есть, сложить строку с числом у вас не получится.
а сконкатенировать?
А если бы в эти аргументы передавались датаклассы, и были бы проставлены соответствующие аннотации,
ужас какой
разве аннотаций недостаточно?
fireSparrow
15.03.2024 06:57>> разве аннотаций недостаточно?
Ну, если у вас словарь, то вы, конечно, можете написать аннотацию dict[string, Any], но толку тут не много, вы всё-равно не знаете, какие ключи в этом словаре. А в датаклассе полностью определён перечень полей и типы их значений.
ednersky
15.03.2024 06:57под аннотациями я имел в виду текстовый комментарий разработчика перед заголовком функции. Возможно на специальном языке разметки
fireSparrow
15.03.2024 06:57В питоне под аннотациями понимают вполне конкретную вещь. В статье речь именно про питоновские аннотации.
fireSparrow
15.03.2024 06:57>> а сконкатенировать?
А что вы вообще подразумеваете под конкатенацией строки и числа применительно к питону? И чем это отличается от сложения строки и числа?
ednersky
15.03.2024 06:57под конкатенацией я подразумеваю конкатенацию
совершенно очевидно (и иных прочтений быть не может) что означает конкатенация:
конкатенация(1, 2)
конкатенация('1', '2')
конкатенация('ф', 'у')
конкатенация('ф', 2)
а под сложением я понимаю сложение
совершенно очевидно (и иных прочтений быть не может) что означает сложение:
сложение(1, 2)
сложение('1', '2')
сложение(1, '2')
И я говорю о проблеме в целом (об исследовании Ларри), а не о Python конкретно.
То, что Python криво задизайнили и теперь пытаются вылечить головную боль методом внедрения гильотин - отдельный разговор.
Хотя у Python есть опыт обратнонесовместимых шагов (Python2 -> 3), могли бы и нормально поправить дизайн.
rSedoy
15.03.2024 06:57+1вы упорно приводите примеры проблемы, которой нет в Python, вам упорно говорят, что тут подняли немного другую проблему
fireSparrow
15.03.2024 06:57>> То, что Python криво задизайнили
А в чём именно вы видите кривой дизайн? В том, что в питоне нельзя по ошибке применить конкатенацию вместо сложения?
>> пытаются вылечить головную боль методом внедрения гильотин
А вот здесь вообще о чем речь? Ещё раз повторюсь - в питоне вообще нет никакой путаницы сложения и конкатенации. Поэтому в питоне никто не пытается решить эту проблему, за отсутствием проблемы. Аннотации используются вообще для другого.
>> И я говорю о проблеме в целом (об исследовании Ларри), а не о Python конкретно.
О какой проблеме-то? Во многих языках (включая питон) нет проблемы с неоднозначностью сложения/конкатенации, а польза от типов - есть.
И типы, внезапно, нужны не только для того, чтобы сделать счастливым компилятор. А ещё и для того, чтобы сделать работу программиста проще и сократить число ошибок.
И, кстати, почему вы упорно называете Ларри Уолла только по имени? Вы с ним лично знакомы?
ednersky
15.03.2024 06:57А в чём именно вы видите кривой дизайн? В том, что в питоне нельзя по ошибке применить конкатенацию вместо сложения?
Именно что можно. Ведь оператора конкатенации в Python нет, там есть оператор "плюс", который выполняет и операцию сложения и операцию конкатенации.
отсюда следует то, что программист начинает быть обязан приводить типы к одному виду, прежде, чем он вызовет нужный ему оператор.
и именно отсюда следует вся эта возня с типами в бестиповом языке.
проблема давно исследована и правильное решение найдено: нужно использовать операторы только для того, для чего они предназначены.
если вы пишете функцию, вычисляющую факториал, то не нужно пытаться чтобы попутно она считала и числа Фибоначчи - написать такое можно, но в итоге получится нечто неправильное, кривое, что трудно исправлять.
ровно так и с операторами конкатенации и сложения
ednersky
15.03.2024 06:57И типы, внезапно, нужны не только для того, чтобы сделать счастливым
компилятор.только для этого
А ещё и для того, чтобы сделать работу программиста проще и сократить число ошибок.
каким же образом эта работа становится проще, если вместо строки кода программист должен писать экраны?
fireSparrow
15.03.2024 06:57+1Вы когда-нибудь занимались поддержкой проекта на 100000+ строк, в который два десятка программистов несколько лет писали?
Я бы посмотрел, как вы это будете без типов делать.
ednersky
15.03.2024 06:57ага
занимался
а потом занимался проектами с типами
так вот с последними гемора куда больше
gromyko21 Автор
15.03.2024 06:57+1Расскажи в чем конкретно был гемор? Неправильно определили типы?
ednersky
15.03.2024 06:57гемор в том, что вместо решения проблем проекта масса времени тратится на работу с типами
это как пытаться bash заменить на С++
на bash 4 строки
на С++ 4 модуля, плюс система сборки, компилятор итп
конечно bash vs C++ это предельный кейс, но именно он показывает никчемность, ненужность типов
Вы когда-нибудь занимались поддержкой проекта на 100000+
вы когда-нибудь задумывались, что цифра 100000+ может быть сокращена впятеро-десятеро простым отказом от типов?
gromyko21 Автор
15.03.2024 06:57+1Я, в случае, если метод принимает 3+ аргумента сразу разному каждый из них на новую строку, для лучшей читаемости, а рядом ставлю тип, что никак не увеличивает количество строк.
Если говорить про 1-2 аргумента - оставляю их на одной строке и на ней же добавляю типы, что так же никак не увеличивает количество строк.
fireSparrow
15.03.2024 06:57+3Я не знаю, что у вас за специфичный проект такой был.
В моей практике большинство проектов пишутся за достаточно небольшой период времени, а потом годы занимает их поддержка и доработка. И поэтому основную часть времени разработчик читает код, а не пишет. В таком режиме хорошо расставленные типы могут сократить затраченное разработчиком время в десятки раз. И значительно сократить количество багов при изменении существующего кода.
Ну и расстановка типов не увеличивает как-то драматически ни объём кода, ни затраченное время на его написание.Уж точно не в разы, даже если сказать, что на 10% - то это достаточно пессимистичная оценка.
ednersky
15.03.2024 06:57И поэтому основную часть времени разработчик читает код, а не пишет.
Именно так. и потому за типы, за необоснованный ООП, за рекурсии, и за многое другое прямо бить по рукам иногда хочется.
fireSparrow
15.03.2024 06:57+1>> за необоснованный ООП
За необоснованное что угодно можно бить по рукам.
Но при правильном использовании и ООП, и типы, облегчают работу с кодом, а не усложняют. Если у вас типы и ООП только мешают - то либо вы работаете с какими совсем уж специфичными кейсами, либо вы просто не умеете их готовить.
ednersky
15.03.2024 06:57ООП в 95% случаев оказывается необоснован
типы необоснованы в 100% случаев, где решение на скриптовом языке возможно
ednersky
15.03.2024 06:57Поддерживать код, у которого не размечены типы - очень-очень больно.
поддерживать код, у которого они размечены, куда больнее!
ибо там где без типов 3 строки кода, с типами получается 3 экрана
ведь что Вы предлагаете?
вместо вызова функции сперва вызывать функции-конструкторы датасетов, которые станут аргументами функции
а эти конструкторы тоже через датасеты?
Бр-р-р!
fireSparrow
15.03.2024 06:57В питоне для датаклассов обычно не используются какие-то отдельные функции конструкторы. А само по себе инстанцирование датакласса занимает примерно столько же места, сколько и создание словаря.
Andrey_Solomatin
15.03.2024 06:57Это больше похоже на плохую архитектуру, язык здесь вторичен.
Мапы объектов можно и в типизированных языках передавать.
Если нет требования по производительности, просто делайте всё неизменяемым и создавайте обхект нового типа на каждной стадии. Если нет, то паттерн строитель.
Датаклассы не очень помогут, Any | None поменятеся на int | None и поля будут определены. Если вы не можете посчитать состояния объекта по пальцам одной руки, это уже сложно для восприятия. Два optional поля дают 4 варианта состояния. None(еще на заполнен) и None(Заплненное значение) это два разных стостояния, но из вообще не отличить.
longclaps
15.03.2024 06:57С момента создания питона разработчики языков додумались, удивительное дело, в статически типизированных языках типа Kotlin выводить тип переменной из вида (написания) присваемого значения. Меньше синтаксического мусора, почти как в питоне).
Вольное присваивание аргументам функций в питоне значений произвольного типа - это удобное решение для скриптовых языков. В котлине сходной гибкости и выразительности добиваются созданием нескольких одноименных функций с разной сигнатурой, но писанины больше. Аннотирование функций в питоне подравнивает ситуацию по объёму писанины, но котлином его не делает, в смысле строгости и производительности. Так что присоединяюсь к автору статьи: хочешь типов - присмотрись к другим языкам.
Andrey_Solomatin
15.03.2024 06:57Аннотирование функций в питоне подравнивает ситуацию по объёму писанины
Это в простых случаях. А в сложных в Питоне просто забиваешь и работает, а в статических языках придётся решать. А там это делать куда сложнее.gromyko21 Автор
15.03.2024 06:57Сложнее и дольше - да. Но на действительно больших проектах это будет работать стабильнее)
Sap_ru
15.03.2024 06:57+2А ничего, что это все пришло из C/C++, где можно перегружать функции почти произвольным образом? Правда поломать совсем всё не даст жёсткая типизация. Проблема вовсе не в том, что можно обсудить функции с разной сигнатурой, но одним именем. Котлин тут отстаёт. Проблема в слабой типизации. А у Python еще и в, что перегрузка сделана через такие же костыли, что и все остальные типы, и реализация таких функции ведёт к небходимости избегать строгой проверки типов и куче трудно уловимых ошибок.
tenzink
15.03.2024 06:57У python всё-таки типизация сильная (нельзя сложить яблоки со столами), но динамическая. То есть до момента выполнения в общем случае вы не знаете точно пытаетесь ли складывать яблоки со столами
Sap_ru
15.03.2024 06:57То, что программа закрашится, это (наверное) хорошо. Но она откомпилируется и запустится. И может даже пройти все тесты, а закрашиться потом в самый неподходящий момент. Особенно актуально для перегрузки функций, где весь лоск немедленно слетает и заход солнца программист неизменно выполняет вручную.
tenzink
15.03.2024 06:57Именно поэтому предпочитаю писать на языках со статической типизацией, чтобы компилятор бил по рукам. Да и в python я бы предпочёл статическую типизацию, если бы она там была
Andrey_Solomatin
15.03.2024 06:57Проблема в слабой типизации.
В Питоне сильная типизация. А еще она динамическая.
redfox0
15.03.2024 06:57К сожалению, в Python типизация не дает никакого прироста производительности, а только потенциальное замедление и увеличение потребления памяти.
Файлы *.pyi решают проблему "потенциального замедления и увеличения потребления памяти" и можно писать аннотаций сколько хочешь.
Код картинками жгёт. Проверил, аннотации не вырезаются из оптимизированного кода, в отличии от
__doc__
иassert
.# python3 -OOO file.py def custom_sum(first: int, second: int) -> int: return first + second print(custom_sum.__annotations__)
gromyko21 Автор
15.03.2024 06:57Спасибо про замечание с кодом с картинками. Это моя первая подобная статья и я вообще не подумал, что их можно вставлять в виде текста и так будет комфортнее читать.
fireSparrow
15.03.2024 06:57Ну, логично, что аннотации не вырезаются.
Ведь они являются полноценным атрибутом, и программист вполне может завязать какую-то логику на проверку аннотаций функции. Было бы странно, если бы оптимизация ломала такую логику.
Vindicar
15.03.2024 06:57Я воспринимаю аннотации типов скорее как декларативное описание "что в этом параметре/этой переменной". Условно, код на C-подобном языке
float speed
содержит описание типа, но не содержит сведения о том, как это значение интерпретировать - как метры в секунду или как километры в час. Тут ближе подходит концепция доменных типов, конечно - но аннотации в питоне являются приемлемым промежуточным решением, и позволяют добиться хоть какой-то ясности, не трогая саму логику.Что касается вышеупомянутой проблемы со словарями - есть TypedDict, который позволяет, по сути, описать схему для словаря (по аналогии со схемой JSON).
vilgeforce
15.03.2024 06:57" типы из библиотеки typing по типу List " - в более современных версиях питона list - встроенный тип и его не надо испортировать из typing, как и dict
gromyko21 Автор
15.03.2024 06:57Есть такое) С модулем typing, как по мне получился более выразительный пример. Хорошо, что не останавливался на этом вопросе, а решил его опустить
NN1
15.03.2024 06:57+1Типы из typing потихоньку переходят в разряд устаревших.
Новые типы имеют поддержку в рантайме позволяя получить полный тип list[int] вместо List.
https://docs.python.org/3/library/typing.html#deprecated-aliases
Andrey_Solomatin
15.03.2024 06:57Кроме коллкеций который переехали, там есть и другие вещи.
Хотя большую часть из них я боюсь использовать в продакшен коде, сложновато. Питон не главный язык в команде и не все в него глубоко погружены.NN1
15.03.2024 06:57Если есть 3.9 и выше, а скорее всего так уже и есть, стоит использовать новые аннотации вместо устаревших.
NN1
15.03.2024 06:57По умолчанию у MyPy довольно щадящие настройки.
https://careers.wolt.com/en/blog/tech/professional-grade-mypy-configuration
Можно немного сделать строже.
Andrey_Solomatin
15.03.2024 06:57Можно видеть проблему “опережающей ссылки”. Наш класс в одном из
методов хочет вернуть экземпляр самого себя, но не сможет этого сделать,
поскольку объект класса еще не определен, пока Python не закончит
вычисление его тела. В этом случае мы вынуждены записать возвращаемое
значение в виде строки.
Приветствую тебя гость из прошлого.
В 3.9 можно вот так:from __future__ import annotations
omaxx
15.03.2024 06:57Вы поспешили написать этот комментарий до того как прочли следующий абзац?
Одобренный PEP 563 “Postponed Evaluation of Annotations” уменьшил время необходимое для обработки аннотаций типов во время выполнения. Аннотации типов больше не вычисляются в момент определения функции, вместо этого они сохраняются в аннотациях в строковой форме(не производя никаких вычислений). Чтобы достичь этого нужно сделать один import.
Andrey_Solomatin
15.03.2024 06:57Да, только проверил дату публикации.
Убирать не стал, так как второй обзац сфокусирован на другой проблеме и про что он решает "опережающей ссылки" нужно самому додуматься.Посмотрел доку, а этот PEP до сих пор не включили в Питон.
Я его перепутал с https://peps.python.org/pep-0604/ которого мне в 3.9 не хватет.
tumbler
По опыту, большинство аннотаций помогают тому же PyCharm с автокомплитом - что несомненно плюс.
Использование MyPy может найти пару ошибок в аннотациях (но и только!) - тут скорее минус, с учетом времени, которое можно потратить на попытки убедить MyPy в том, что ты правильно всё аннотировал.
gromyko21 Автор
Да, порой с MyPy приходится потанцевать, но мое мнение - он все же помогает повысить стабильность системы.