Хабрахабр

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

Сегодня мы публикуем вторую часть перевода материала, который посвящён аннотациям типов в Python.

→ Первая часть

Как Python поддерживает работу с типами данных?

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

Байт-код состоит из инструкций, которые, по своей сути, похожи на процессорные инструкции. Вот что происходит при подготовке Python-кода к выполнению: «В Python исходный код преобразуется, с использованием CPython, в гораздо более простую форму, называемую байт-кодом. (Здесь речь идёт не о тех виртуальных машинах, возможности которых позволяют запускать в них целые операционные системы. Но они выполняются не процессором, а программной системой, которая называется виртуальной машиной. В нашем случае это среда, которая представляет собой упрощённую версию окружения, доступного программам, выполняемым на процессоре)».

Ведь мы этих типов не указывали. Откуда CPython знает о том, какими должны быть типы переменных, когда готовит программу к выполнению? Он знает лишь то, что переменные — это объекты. CPython об этом и не знает. Всё в Python — это объект, по крайней мере, до тех пор, пока не оказывается, что нечто имеет более конкретный тип.

Если Python встречает число — он считает, что соответствующее значение имеет числовой тип. Например, Python считает строкой всё, что заключено в одинарные или двойные кавычки. Если мы попытаемся сделать с некоей сущностью что-то такое, что нельзя делать с сущностью её типа, Python сообщит нам об этом позже.

Рассмотрим следующее сообщение об ошибке, выводящееся при попытке сложить строку и число:

name = 'Vicki'
seconds = 4.71; ---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-9-71805d305c0b> in <module> 3 4 ----> 5 name + seconds TypeError: must be str, not float

Система сообщает нам о том, что она не может складывать строки и числа с плавающей точкой. При этом то, что name — это строка, а seconds — это число, не интересовало систему до тех пор, пока не была выполнена попытка сложить name и seconds.

Python не интересует то, какой именно тип имеет некий объект. Другими словами, это можно описать так: «Утиная типизация используется при выполнении сложения. Если это не так — выдаётся ошибка». Всё, что интересует систему — это то, возвращает ли что-то осмысленное вызов метода сложения.

Это значит, что мы, если пишем программы на Python, не получим сообщение об ошибке до тех пор, пока интерпретатор CPython не займётся выполнением той самой строки, в которой имеется ошибка. Что бы это значило?

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

Собственно говоря, тут мы и подходим к разговору об аннотациях типов в Python.

Если вы работаете со сложными структурами данных или с функциями, принимающими множество входных значений, использование аннотаций значительно упрощает работу с подобными структурами и функциями. Можно сказать, что, в целом, у использования аннотаций типов есть множество сильных сторон. Если у вас имеется лишь единственная функция с одним параметром, как в приведённых здесь примерах, то работать с такой функцией, в любом случае, очень просто. Особенно — через некоторое время после их создания.

Что если нам нужно работать со сложными функциями, принимающими множество входных значений, которые похожи на эту, из документации PyTorch:

def train(args, model, device, train_loader, optimizer, epoch): model.train() for batch_idx, (data, target) in enumerate(train_loader): data, target = data.to(device), target.to(device) optimizer.zero_grad() output = model(data) loss = F.nll_loss(output, target) loss.backward() optimizer.step() if batch_idx % args.log_interval == 0: print('Train Epoch: [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format( epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))

Что такое model? Мы, конечно, можем покопаться в кодовой базе и это выяснить:

model = Net().to(device)

Но хорошо было бы, если можно было бы просто указать тип model в сигнатуре функции и избавить бы себя от ненужного анализа кода. Возможно, это выглядело бы так:

def train(args, model (type Net), device, train_loader, optimizer, epoch):

А как насчёт device? Если порыться в коде — можно выяснить следующее:

device = torch.device("cuda" if use_cuda else "cpu")

Теперь перед нами встаёт вопрос о том, что такое torch.device. Это — специальный тип PyTorch. Его описание можно найти в соответствующем разделе документации к PyTorch.

Тем самым мы сэкономили бы немало времени тому, кому пришлось бы анализировать этот код. Хорошо было бы, если бы мы могли указать тип device в списке аргументов функции.

def train(args, model (type Net), device (type torch.Device), train_loader, optimizer, epoch):

Эти рассуждения можно продолжать ещё очень долго.

Но они приносят пользу и тем, кто читает чужой код. В результате оказывается, что аннотации типов весьма полезны для того, кто пишет код. Аннотации типов улучшают читабельность кода. Гораздо легче читать типизированный код, чем код, для понимания которого приходится разбираться с тем, что представляет собой та или иная сущность.

Итак, что же сделано в Python для того, чтобы вывести код на тот же уровень читабельности, которым отличается код, написанный на статически типизированных языках?

Аннотации типов в Python

Теперь мы готовы к тому, чтобы серьёзно поговорить об аннотациях типов в Python. Читая программы, написанные на Python 2, можно было видеть, что программисты снабжали свой код подсказками, сообщающими читателям кода о том, какой тип имеют переменные или значения, возвращаемые функциями.

Подобный код изначально выглядел так:

users = [] # type: List[UserID]
examples = {} # type: Dict[str, Any]

Аннотации типов раньше представляли собой простые комментарии. Но случилось так, что Python начал постепенно сдвигаться в сторону более единообразного способа обращения с аннотациями. В частности, речь идёт о появлении документа PEP 3107, посвящённого аннотированию функций. 

Этот документ, посвящённый аннотациям типов, разрабатывался в тесной связи с mypy — проектом DropBox, который направлен на проверку типов перед запуском скриптов. Далее, началась работа над PEP 484. Сообщение об ошибке во время выполнения можно получить если, например, попробовать сделать со значением некоего типа то, что этот тип не поддерживает. Пользуясь mypy, стоит помнить о том, что проверка типов не производится во время выполнения скрипта. Скажем — если попытаться сделать срез словаря или вызвать метод .pop() для строки.

Вместо этого данное предложение предусматривает существование отдельного самостоятельного инструмента для проверки типов, с помощью которого пользователь, по своему желанию, может проверять исходный код своих программ. Вот что можно узнать из PEP 484 о деталях реализации аннотаций: «Хотя эти аннотации доступны во время выполнения программы через обычный атрибут annotations, во время выполнения проверки типов не производятся. (Хотя, конечно, отдельные пользователи могут применить похожий инструмент для проверки типов и во время выполнения программы — ради реализации методологии Design By Contract, или ради выполнения JIT-оптимизации. В целом, подобный инструмент для проверки типов работает как очень мощный линтер. Но надо отметить, что подобные инструменты пока не достигли достаточной зрелости».

Как же выглядит работа с аннотациями типов на практике?

Так, PyCharm, предлагает, на базе сведений о типах, автозавершение кода и выполнение его проверок. Например, их применение означает возможность облегчения работы в различных IDE. Похожие возможности имеются и в VS Code.

Вот отличный пример подобной защиты. Аннотации типов полезны и по ещё одной причине: они защищают разработчика от глупых ошибок.

Предположим, мы добавляем в словарь имена людей:

names = {'Vicki': 'Boykis', 'Kim': 'Kardashian'} def append_name(dict, first_name, last_name): dict[first_name] = last_name append_name(names,'Kanye',9)

Если мы подобное позволим — в словаре окажется множество неправильно сформированных записей.
Исправим это:

from typing import Dict names_new: Dict[str, str] = {'Vicki': 'Boykis', 'Kim': 'Kardashian'} def append_name(dic: Dict[str, str] , first_name: str, last_name: str): dic[first_name] = last_name append_name(names_new,'Kanye',9.7) names_new

Теперь проверим этот код с помощью mypy и получим следующее:

(kanye) mbp-vboykis:types vboykis$ mypy kanye.py
kanye.py:9: error: Argument 3 to "append_name" has incompatible type "float"; expected "str"

Видно, что mypy не позволяет использовать число там, где ожидается строка. Тем, кто хочет пользоваться подобными проверками на регулярной основе, рекомендуется включить mypy туда, где в их системах непрерывной интеграции производится тестирование кода.

Подсказки типов в различных IDE

Одно из важнейших преимуществ применения аннотаций типов заключается в том, что они позволяют Python-программистам пользоваться теми же возможностями по автозавершению кода в различных IDE, которые доступны для статически типизированных языков.

Это — пара функций из предыдущих примеров, обёрнутых в классы. Например, предположим, что у вас имеется фрагмент кода, напоминающий следующий.

from typing import Dict class rainfallRate: def __init__(self, hours, inches): self.hours= hours self.inches = inches def calculateRate(self, inches:int, hours:int) -> float: return inches/hours rainfallRate.calculateRate() class addNametoDict: def __init__(self, first_name, last_name): self.first_name = first_name self.last_name = last_name self.dict = dict def append_name(dict:Dict[str, str], first_name:str, last_name:str): dict[first_name] = last_name addNametoDict.append_name()

Приятно то, что мы, после того, как по своей инициативе добавили в код описания типов, можем наблюдать за тем, что происходит в программе при вызове методов классов:

Подсказки по типам в IDE

Начало работы с аннотациями типов

В документации к mypy можно найти хорошие рекомендации, касающиеся того, с чего лучше начать, приступая к типизации кодовой базы:

  1. Начните с малого — добейтесь того, чтобы некоторые файлы, содержащие несколько аннотаций, проходили бы проверку с помощью mypy.
  2. Напишите скрипт для запуска mypy. Это поможет добиться единообразных результатов испытаний.
  3. Запускайте mypy в CI-конвейерах для предотвращения ошибок, связанных с типами.
  4. Постепенно аннотируйте модули, которые используются в проекте чаще всего.
  5. Добавляйте аннотации типов в существующий код, который вы модифицируете; оснащайте ими новый код, который пишете.
  6. Используйте MonkeyType или PyAnnotate для автоматического аннотирования старого кода.

Вам, прежде чем приступить к аннотированию собственного кода, полезно будет кое с чем разобраться.

Во-первых, вам понадобится импортировать в код модуль typing в том случае, если вы пользуетесь чем-то помимо строк, целых чисел, логических значений и значений других базовых типов Python.

Среди них — Dict, Tuple, List и Set. Во-вторых, этот модуль даёт возможность работать с несколькими сложными типами. Ещё существуют типы, которые называются Optional и Union. Конструкция вида Dict[str, float] означает, что вы хотите работать со словарём, в элементах которого, в качестве ключа, используется строка, а в качестве значения — число с плавающей точкой.

В-третьих — вам нужно ознакомиться с форматом аннотаций типов:

import typing def some_function(variable: type) -> return_type: do_something

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

Итоги. Стоит ли пользоваться аннотациями типов в Python?

Сейчас давайте зададимся вопросом о том, стоит ли вам пользоваться аннотациями типов в Python. На самом деле, это зависит от особенностей вашего проекта. Вот что по этому поводу говорит Гвидо ван Россум в документации к mypy: «Цель mypy заключается не в том, чтобы убедить всех в необходимости писать статически типизированный Python-код. Статическая типизация — это, и сейчас, и в будущем, совершенно необязательно. Цель mypy заключается в том, чтобы дать Python-программистам больше возможностей. В том, чтобы сделать Python более конкурентоспособной альтернативой другим статически типизированным языкам, используемым в больших проектах. В том, чтобы повысить производительность труда программистов и улучшить качество программного обеспечения».

Какой проект считать маленьким? Затраты времени, нужные для настройки mypy и для планирования типов, необходимых для некоей программы, не оправдывают себя в маленьких проектах и при проведении экспериментов (например, производимых в Jupyter). Вероятно, такой, объём которого, по осторожным подсчётам, не превышает 1000 строк.

Там они могут, в частности, сэкономить немало времени. Аннотации типов имеют смысл в более крупных проектах. Речь идёт о проектах, разрабатываемых группами программистов, о пакетах, о коде, при разработке которого используются системы контроля версий и CI-конвейеры.

И я полагаю, что тот, кто начнёт работать с ними раньше других, ничего не проиграет. Я полагаю, что аннотации типов в ближайшие пару лет станут гораздо распространённее, чем сейчас, не говоря уже о том, что они вполне могут превратиться в обычный повседневный инструмент.

Уважаемые читатели! Пользуетесь ли вы аннотациями типов в своих Python-проектах?

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

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

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

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

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