Сегодня мы публикуем вторую часть перевода материала, который посвящён аннотациям типов в 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, посвящённого аннотированию функций.
Далее, началась работа над PEP 484. Этот документ, посвящённый аннотациям типов, разрабатывался в тесной связи с mypy — проектом DropBox, который направлен на проверку типов перед запуском скриптов. Пользуясь mypy, стоит помнить о том, что проверка типов не производится во время выполнения скрипта. Сообщение об ошибке во время выполнения можно получить если, например, попробовать сделать со значением некоего типа то, что этот тип не поддерживает. Скажем — если попытаться сделать срез словаря или вызвать метод .pop()
для строки.
Вот что можно узнать из PEP 484 о деталях реализации аннотаций: «Хотя эти аннотации доступны во время выполнения программы через обычный атрибут annotations
, во время выполнения проверки типов не производятся. Вместо этого данное предложение предусматривает существование отдельного самостоятельного инструмента для проверки типов, с помощью которого пользователь, по своему желанию, может проверять исходный код своих программ. В целом, подобный инструмент для проверки типов работает как очень мощный линтер. (Хотя, конечно, отдельные пользователи могут применить похожий инструмент для проверки типов и во время выполнения программы — ради реализации методологии Design By Contract, или ради выполнения JIT-оптимизации. Но надо отметить, что подобные инструменты пока не достигли достаточной зрелости».
Как же выглядит работа с аннотациями типов на практике?
Например, их применение означает возможность облегчения работы в различных IDE. Так, PyCharm, предлагает, на базе сведений о типах, автозавершение кода и выполнение его проверок. Похожие возможности имеются и в 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 можно найти хорошие рекомендации, касающиеся того, с чего лучше начать, приступая к типизации кодовой базы:
- Начните с малого — добейтесь того, чтобы некоторые файлы, содержащие несколько аннотаций, проходили бы проверку с помощью mypy.
- Напишите скрипт для запуска mypy. Это поможет добиться единообразных результатов испытаний.
- Запускайте mypy в CI-конвейерах для предотвращения ошибок, связанных с типами.
- Постепенно аннотируйте модули, которые используются в проекте чаще всего.
- Добавляйте аннотации типов в существующий код, который вы модифицируете; оснащайте ими новый код, который пишете.
- Используйте MonkeyType или PyAnnotate для автоматического аннотирования старого кода.
Вам, прежде чем приступить к аннотированию собственного кода, полезно будет кое с чем разобраться.
Во-первых, вам понадобится импортировать в код модуль typing в том случае, если вы пользуетесь чем-то помимо строк, целых чисел, логических значений и значений других базовых типов Python.
Во-вторых, этот модуль даёт возможность работать с несколькими сложными типами. Среди них — Dict
, Tuple
, List
и Set
. Конструкция вида Dict[str, float]
означает, что вы хотите работать со словарём, в элементах которого, в качестве ключа, используется строка, а в качестве значения — число с плавающей точкой. Ещё существуют типы, которые называются Optional
и Union
.
В-третьих — вам нужно ознакомиться с форматом аннотаций типов:
import typing
def some_function(variable: type) -> return_type:
do_something
Если вы хотите узнать подробности о том, как начать применять аннотации типов в своих проектах, мне хотелось бы отметить, что этому посвящено немало хороших руководств. Вот — одно из них. Я считаю его самым лучшим. Освоив его, вы узнаете об аннотировании кода и о его проверке.
Итоги. Стоит ли пользоваться аннотациями типов в Python?
Сейчас давайте зададимся вопросом о том, стоит ли вам пользоваться аннотациями типов в Python. На самом деле, это зависит от особенностей вашего проекта. Вот что по этому поводу говорит Гвидо ван Россум в документации к mypy: «Цель mypy заключается не в том, чтобы убедить всех в необходимости писать статически типизированный Python-код. Статическая типизация — это, и сейчас, и в будущем, совершенно необязательно. Цель mypy заключается в том, чтобы дать Python-программистам больше возможностей. В том, чтобы сделать Python более конкурентоспособной альтернативой другим статически типизированным языкам, используемым в больших проектах. В том, чтобы повысить производительность труда программистов и улучшить качество программного обеспечения».
Затраты времени, нужные для настройки mypy и для планирования типов, необходимых для некоей программы, не оправдывают себя в маленьких проектах и при проведении экспериментов (например, производимых в Jupyter). Какой проект считать маленьким? Вероятно, такой, объём которого, по осторожным подсчётам, не превышает 1000 строк.
Аннотации типов имеют смысл в более крупных проектах. Там они могут, в частности, сэкономить немало времени. Речь идёт о проектах, разрабатываемых группами программистов, о пакетах, о коде, при разработке которого используются системы контроля версий и CI-конвейеры.
Я полагаю, что аннотации типов в ближайшие пару лет станут гораздо распространённее, чем сейчас, не говоря уже о том, что они вполне могут превратиться в обычный повседневный инструмент. И я полагаю, что тот, кто начнёт работать с ними раньше других, ничего не проиграет.
Уважаемые читатели! Пользуетесь ли вы аннотациями типов в своих Python-проектах?
Автор: ru_vds