C 2014 года, когда в Python появилась поддержка аннотаций типов, программисты работают над их внедрением в свой код. Автор материала, первую часть перевода которого мы публикуем сегодня, говорит, что по её оценке, довольно смелой, сейчас аннотации типов (иногда их называют «подсказками») используются примерно в 20-30% кода, написанного на Python 3. Вот результаты опроса, который она, в мае 2019, провела в Twitter.
Как оказалось, аннотации используют 29% респондентов. По словам автора статьи, в последние годы она всё чаще натыкается на аннотации типов в различных книгах и учебных руководствах.
В документации по Python термины «type hint» («подсказка типа») и «type annotation» («аннотация типа») используются как взаимозаменяемые. Автор статьи пользуется, в основном, термином «type hint», мы — термином «аннотация типа».
В этом материале будет рассмотрен широкий спектр вопросов, касающихся использования аннотаций типов в Python. Если вы хотите обсудить оригинал статьи с автором — можете воспользоваться механизмом pull-запросов.
Введение
Здесь можно найти классический пример того, как выглядит код, написанный с использованием аннотаций типов.
Вот обычный код:
def greeting(name):
return 'Hello ' + name
Вот код, в который добавлены аннотации:
def greeting(name: str) -> str:
return 'Hello ' + name
Шаблон, в соответствии с которым оформляется код с аннотациями типов, выглядит так:
def function(variable: input_type) -> return_type:
pass
На первый взгляд кажется, что применение аннотаций типов в коде выглядит просто и понятно. Но в сообществе разработчиков всё ещё присутствует изрядная доля неопределённости в понимании того, чем именно являются аннотации типов. Кроме того, неясность есть даже и в том, как их правильно называть — «аннотациями» или «подсказками», и в том, какие плюсы даёт их использование в кодовой базе.
Когда я начала исследовать эту тему и думать о том, нужно ли мне использовать аннотации типов, я чувствовала себя совершенно запутанной. В результате я решила поступить так же, как поступаю всегда, встретившись с чем-то непонятным. Я решила глубоко исследовать этот вопрос и изложила свои изыскания в виде этого материала, который, надеюсь, пригодится тем, кто, как и я, хочет разобраться с аннотациями типов в Python.
Как компьютеры выполняют наши программы?
Для того чтобы понять то, чего разработчики Python пытаются добиться с помощью аннотаций типов, давайте поговорим о механизмах компьютерных систем, которые находятся на несколько уровней ниже Python-кода. Благодаря этому мы сможем лучше понять то, как, в общих чертах, работают компьютеры и языки программирования.
Языки программирования, в своей основе, это инструменты, которые позволяют обрабатывать данные средствами центрального процессора (CPU), а также хранить в памяти данные, которые нужно обработать, и данные, получающиеся в результате обработки.
Упрощённая схема компьютера
Процессор — это штука, по своей сути, довольно «глупая». Он способен выполнять впечатляющие действия с данными, но он понимает лишь машинные команды, которые сводятся к наборам электрических сигналов. Машинный язык можно представить состоящим из нулей и единиц.
Для того чтобы подготовить эти нули и единицы, понятные процессору, нужно перевести код с языка высокого уровня на язык низкого уровня. Именно здесь за дело берутся компиляторы и интерпретаторы.
Если язык является компилируемым или выполняемым (Python-код выполняется средствами интерпретатора), то код на этом языке превращается в низкоуровневый машинный код, который содержит инструкции для низкоуровневых компонентов компьютера, то есть — для аппаратного обеспечения.
Существует несколько способов перевода кода, написанного на некоем языке программирования, в код, понятный машинам. Можно либо создать файл с кодом программы и преобразовать его в машинный код с помощью компилятора (так работают C++, Go, Rust и некоторые другие языки), либо запустить код напрямую с помощью интерпретатора, который и будет отвечать за преобразование кода в машинные команды. Именно так, с помощью интерпретаторов, запускаются программы на Python, а также — на других «скриптовых» языках, таких, как PHP и Ruby.
Схема обработки кода интерпретируемых языков
Откуда аппаратное обеспечение знает о том, как хранить в памяти нули и единицы, представляющие данные, с которыми работает программа? О том, как выделять память для этих данных, компьютеру должна сообщить наша программа. А что это за данные? Это зависит от того, какие типы данных поддерживает тот или иной язык.
Типы данных имеются во всех языках. Обычно типы данных — это одна из первых тем, которую изучают новички, которые осваивают программирование на некоем языке.
Существуют прекрасные руководства по тому же Python, например — это, в которых можно найти подробные сведения о типах данных. Если говорить простыми словами, то типы данных — это разные способы представления данных, размещаемых в памяти.
Среди существующих типов данных можно отметить, например, строки и целые числа. Набор доступных разработчику типов данных зависит от используемого им языка программирования. Вот, например, список базовых типов данных Python:
int, float, complex
str
bytes
tuple
frozenset
bool
array
bytearray
list
set
dict
Существуют и типы данных, состоящие из других типов данных. Например, список (list
) в Python может хранить целые числа или строки, а также и то, и другое.
Для того чтобы узнать о том, сколько памяти требуется выделить для хранения неких данных, компьютеру нужно знать о том, данные какого типа собирается разместить в памяти программа. В Python имеется встроенная функция getsizeof
, которая позволят нам узнать об объёме памяти, выраженном в байтах, необходимом для хранения значений различных типов данных.
Вот один замечательный ответ на StackOverflow, в котором можно найти сведения о том, как узнать размеры «минимальных» значений, которые могут храниться в переменных различных типов.
import sys
import decimal
import operator
d = {"int": 0,
"float": 0.0,
"dict": dict(),
"set": set(),
"tuple": tuple(),
"list": list(),
"str": "a",
"unicode": u"a",
"decimal": decimal.Decimal(0),
"object": object(),
}
# Создаём новый словарь, записи которого можно отсортировать по их размеру
d_size = {}
for k, v in sorted(d.items()):
d_size[k]=sys.getsizeof(v)
sorted_x = sorted(d_size.items(), key=lambda kv: kv[1])
sorted_x
[('object', 16),
('float', 24),
('int', 24),
('tuple', 48),
('str', 50),
('unicode', 50),
('list', 64),
('decimal', 104),
('set', 224),
('dict', 240)]
В результате, отсортировав словарь, содержащий образцы значений различных типов, мы можем узнать о том, что максимальный размер имеет пустой словарь (dict
) а за ним идёт множество (set
). В сравнении с ними для хранения одного целого числа (тип int
) нужно совсем мало места.
Вышеприведённый пример даёт нам представление о том, сколько памяти требуется на хранение различных значений, используемых в программах.
Почему нас вообще должно это беспокоить? Дело в том, что некоторые типы лучше других подходят для решения некоторых задач, позволяя решать эти задачи эффективнее. В некоторых ситуациях нужно тщательно проверять типы. Например, иногда делаются проверки того, что используемые в программе типы данных не идут вразрез с некоторыми допущениями, принятыми при проектировании программы.
Но что они такое — это типы? Почему они нам нужны? Здесь в игру вступает такое понятие, как «система типов».
Введение в системы типов
Давным давно, в далёкой далёкой галактике люди, вручную выполнявшие математические вычисления, поняли, что если они сопоставят с числами или элементам уравнений «типы», они смогут снизить количество логических ошибок, появляющихся при выводе математических доказательств относительно этих элементов.
Так как в самом начале информатика сводилась, в сущности, к выполнению больших объёмов ручных вычислений, некоторые из тех давних принципов были перенесены и на эти вычисления. Системы типов стали инструментом, используемым для уменьшения количества ошибок в программах за счёт назначения различным переменным или элементам подходящих типов.
Вот несколько примеров:
- Если мы пишем программное обеспечение для банка, то мы не можем использовать строки во фрагменте кода, который вычисляет остаток по чьему-то счёту.
- Если мы работаем с данными некоего опроса и хотим понять, положительно или отрицательно некто ответил на какой-то вопрос, то ответы «да» и «нет» логичнее всего будет закодировать с использованием логического типа.
- Занимаясь разработкой большой поисковой системы, мы должны ограничивать количество символов, которые пользователи этой системы могут вводить в поле поискового запроса. Это означает, что нам нужно выполнять проверку некоторых данных строкового типа на соответствие определённым параметрам.
В наши дни в программировании выделяют две основные системы типов. Вот что пишет об этом Стив Клабник: «Статическая система типов — это механизм, с помощью которого компилятор проверяет исходный код и назначает метки (называемые «типами») фрагментам программы, а затем использует их для того, чтобы делать выводы о поведении программы. Динамическая система типов — это механизм, с помощью которого компилятор генерирует код для наблюдения за тем, какие виды данных (они, по стечению обстоятельств, тоже называются «типами») используются программой».
Что это значит? Это значит, что при работе с компилируемыми языками обычно нужно назначать типы сущностей заранее. Благодаря этому компилятор сможет проверить их в ходе компиляции кода и узнать, удастся ли создать осмысленную программу из предоставленного ему исходного кода.
Мне недавно попалось одно разъяснение разницы между статической и динамической типизацией. Вероятно, это лучший текст на данную тему, который мне доводилось читать. Вот его фрагмент: «Раньше я пользовался статически типизированными языками, но последние несколько лет программировал, в основном, на Python. Поначалу использование статической типизации меня несколько раздражало. Возникало такое ощущение, что необходимость объявления типов переменных замедляет работу и принуждает меня к излишне явному выражению моих идей. Python же просто позволял мне делать то, что мне хотелось, даже в том случае, если я случайно делал что-то неправильно. Использовать языки со статической типизацией — это как давать задание кому-то, кто всегда переспрашивает, уточняя мелкие детали того дела, которое ему поручают выполнить. А динамическая типизация — это когда тот, кому дают задание, всегда согласно кивает. При этом возникает ощущение, что он тебя понял. Но иногда нет полной уверенности в том, что тот, кому дано задание, как следует разобрался в том, что от него хотят».
В разговорах о системах типов мне попалось кое-что такое, что я поняла не сразу. А именно, понятия «статическая типизация» и «динамическая типизация» тесно связаны с понятиями «компилируемый язык» и «интерпретируемый язык», но термины «статический» и «компилируемый», а также термины «динамический» и «интерпретируемый» не являются синонимами. Язык может быть динамически типизированным, вроде Python, и при этом компилируемым. Точно так же, язык может быть статически типизированным, вроде Java, но при этом и интерпретируемым (например, в случае с Java, при использовании Java REPL).
Сравнение типов данных в статически и динамически типизированных языках
В чём же заключается разница между типами данных в статически и динамически типизированных языках?
При использовании статической типизации типы надо объявлять заранее. Например, если вы работаете в Java, то ваши программы будут выглядеть примерно так:
public class CreatingVariables {
public static void main(String[] args) {
int x, y, age, height;
double seconds, rainfall;
x = 10;
y = 400;
age = 39;
height = 63;
seconds = 4.71;
rainfall = 23;
double rate = calculateRainfallRate(seconds, rainfall);
}
private static double calculateRainfallRate(double seconds, double rainfall) {
return rainfall/seconds;
}
Обратите внимание на начало программы. Там объявлено несколько переменных, рядом с которыми имеются указания о типах этих переменных:
int x, y, age, height;
double seconds, rainfall;
Кроме того, типы указываются и при объявлениях функций, и при объявлениях их аргументов. Без этих объявлений типов программу нельзя будет скомпилировать. Создавая программы на Java нужно, с самого начала, планировать то, какие типы будут иметь те или иные сущности. В результате компилятор, обрабатывая код таких программ, будет знать о том, что именно ему нужно проверять в процессе формирования машинного кода.
Python избавляет программиста от подобных хлопот. Аналогичный код на Python может выглядеть так:
y = 400
age = 39
height = 63
seconds = 4.71
rainfall = 23
rate = calculateRainfall(seconds, rainfall)
def calculateRainfall(seconds, rainfall):
return rainfall/seconds
Как всё это работает в недрах Python? Продолжение следует…
Уважаемые читатели! Какой язык программирования, из тех, которыми вы пользовались, оставил после себя самые приятные впечатления?
Автор: ru_vds