python

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

  • суббота, 17 августа 2019 г. в 00:17:52
https://habr.com/ru/company/ruvds/blog/463929/
  • Блог компании RUVDS.com
  • Разработка веб-сайтов
  • Python


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? Продолжение следует…

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