Хабрахабр

[Перевод] Исследование глубин аннотаций типов в Python. Часть 1

C 2014 года, когда в Python появилась поддержка аннотаций типов, программисты работают над их внедрением в свой код. Автор материала, первую часть перевода которого мы публикуем сегодня, говорит, что по её оценке, довольно смелой, сейчас аннотации типов (иногда их называют «подсказками») используются примерно в 20-30% кода, написанного на Python 3. Вот результаты опроса, который она, в мае 2019, провела в Twitter.

По словам автора статьи, в последние годы она всё чаще натыкается на аннотации типов в различных книгах и учебных руководствах. Как оказалось, аннотации используют 29% респондентов.

Автор статьи пользуется, в основном, термином «type hint», мы — термином «аннотация типа». В документации по Python термины «type hint» («подсказка типа») и «type annotation» («аннотация типа») используются как взаимозаменяемые.

Если вы хотите обсудить оригинал статьи с автором — можете воспользоваться механизмом pull-запросов.
В этом материале будет рассмотрен широкий спектр вопросов, касающихся использования аннотаций типов в Python.

Введение

Здесь можно найти классический пример того, как выглядит код, написанный с использованием аннотаций типов.

Вот обычный код:

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 = # Создаём новый словарь, записи которого можно отсортировать по их размеру
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 же просто позволял мне делать то, что мне хотелось, даже в том случае, если я случайно делал что-то неправильно. Возникало такое ощущение, что необходимость объявления типов переменных замедляет работу и принуждает меня к излишне явному выражению моих идей. А динамическая типизация — это когда тот, кому дают задание, всегда согласно кивает. Использовать языки со статической типизацией — это как давать задание кому-то, кто всегда переспрашивает, уточняя мелкие детали того дела, которое ему поручают выполнить. Но иногда нет полной уверенности в том, что тот, кому дано задание, как следует разобрался в том, что от него хотят». При этом возникает ощущение, что он тебя понял.

А именно, понятия «статическая типизация» и «динамическая типизация» тесно связаны с понятиями «компилируемый язык» и «интерпретируемый язык», но термины «статический» и «компилируемый», а также термины «динамический» и «интерпретируемый» не являются синонимами. В разговорах о системах типов мне попалось кое-что такое, что я поняла не сразу. Точно так же, язык может быть статически типизированным, вроде Java, но при этом и интерпретируемым (например, в случае с Java, при использовании Java REPL). Язык может быть динамически типизированным, вроде Python, и при этом компилируемым.

Сравнение типов данных в статически и динамически типизированных языках

В чём же заключается разница между типами данных в статически и динамически типизированных языках?

Например, если вы работаете в 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? Продолжение следует…

Уважаемые читатели! Какой язык программирования, из тех, которыми вы пользовались, оставил после себя самые приятные впечатления?

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть