Сегодня мы предлагаем вашему вниманию первую часть перевода материала о том, как в Dropbox занимаются контролем типов Python-кода.
В Dropbox много пишут на Python. Это — язык, который мы используем чрезвычайно широко — как для бэкенд-сервисов, так и для настольных клиентских приложений. Ещё мы в больших объёмах применяем Go, TypeScript и Rust, но Python — это наш главный язык. Если учитывать наши масштабы, а речь идёт о миллионах строк Python-кода, оказалось, что динамическая типизация такого кода неоправданно усложнила его понимание и начала серьёзно влиять на продуктивность труда. Для смягчения этой проблемы мы приступили к постепенному переводу нашего кода на статическую проверку типов с использованием mypy. Это, вероятно, самая популярная самостоятельная система проверки типов для Python. Mypy — это опенсорсный проект, его основные разработчики трудятся в Dropbox.
Dropbox оказалась одной из первых компаний, которая внедрила статическую проверку типов в Python-коде в подобном масштабе. В наши дни mypy используется в тысячах проектов. Этот инструмент бесчисленное количество раз, что называется, «проверен в бою». Нам, для того, чтобы добраться туда, где мы находимся сейчас, пришлось проделать долгий путь. На этом пути было немало неудачных начинаний и провалившихся экспериментов. Этот материал повествует об истории статической проверки типов в Python — с самого её непростого начала, которое было частью моего научного исследовательского проекта, до сегодняшнего дня, когда проверки типов и подсказки по типам стали привычными для бесчисленного количества разработчиков, которые пишут на Python. Эти механизмы теперь поддерживаются множеством инструментов — таких, как IDE и анализаторы кода.
Зачем нужна проверка типов?
Если вы когда-нибудь пользовались динамически типизированным Python — у вас может возникнуть некоторое непонимание того, почему вокруг статической типизации и mypy в последнее время поднялся такой шум. А может быть и так, что Python вам нравится именно из-за его динамической типизации, а происходящее попросту вас расстраивает. Ключ к ценности статической типизации — это масштаб решений: чем больше ваш проект — тем сильнее вы склоняетесь к статической типизации, и, в конце концов, тем сильнее вам это по-настоящему нужно.
Предположим, некий проект достиг размеров в десятки тысяч строк, и оказалось, что над ним работают несколько программистов. Рассматривая подобный проект мы, основываясь на нашем опыте, можем сказать, что понимание его кода станет ключом к поддержке продуктивности разработчиков. Без аннотаций типов непросто бывает выяснить, например, то, какие аргументы нужно передать функции, или то, значения каких типов может некая функция возвращать. Вот типичные вопросы, на которые часто нелегко бывает ответить без применения аннотаций типов:
- Может ли эта функция вернуть
None
? - Чем должен быть этот аргумент
items
? - Каков тип атрибута
id
:int
ли это,str
, или, может, какой-нибудь пользовательский тип? - Должен ли этот аргумент быть списком? Можно ли передать в него кортеж?
Если взглянуть на следующий фрагмент кода, снабжённый аннотациями типов, и попытаться ответить на подобные вопросы, то окажется, что это — простейшая задача:
class Resource:
id: bytes
...
def read_metadata(self,
items: Sequence[str]) -> Dict[str, MetadataItem]:
...
read_metadata
не возвращаетNone
, так как возвращаемый тип не являетсяOptional[…]
.- Аргумент
items
— это последовательность строк. Её нельзя итерировать в произвольном порядке. - Атрибут
id
— это строка байтов.
В идеальном мире можно было бы ожидать, что все подобные тонкости будут описаны во встроенной документации (docstring). Но опыт даёт массу примеров того, что подобной документации в коде, с которым приходится работать, часто не наблюдается. Даже если такая документация в коде и присутствует, нельзя рассчитывать на её абсолютную правильность. Эта документация может быть неясной, неточной, оставляющей массу возможностей для её неправильного понимания. В больших командах или в больших проектах эта проблема может стать крайне острой.
Хотя Python отлично показывает себя на ранних или промежуточных стадиях проектов, в определённый момент успешные проекты и компании, которые используют Python, могут столкнуться с жизненно важным вопросом: «Нужно ли нам переписать всё на статически типизированном языке?».
Системы проверки типов наподобие mypy решают вышеозначенную проблему благодаря тому, что предоставляют в распоряжение разработчика формальный язык для описания типов, и тому, что проверяют то, чтобы описания типов соответствовали бы реализации программ (и, что необязательно, проверяют их существование). В целом можно сказать, что эти системы дают в наше распоряжение нечто вроде тщательно проверенной документации.
У применения подобных систем есть и другие преимущества, и они уже совершенно нетривиальны:
- Система проверки типов может обнаружить некоторые мелкие (а так же — и не особо мелкие) ошибки. Типичный пример — это когда забывают обработать значение
None
или какое-то другое особое условие. - Значительно упрощается рефакторинг кода, так как система проверки типов часто очень точно сообщает о том, какой код нужно изменить. При этом нам не нужно надеяться на 100% покрытие кода тестами, что, в любом случае, обычно невыполнимо. Нам не нужно изучать глубины отчётов трассировки стека для того, чтобы выяснить причину неполадки.
- Даже в больших проектах mypy часто может провести полную проверку типов за доли секунды. А выполнение тестов обычно занимает десятки секунд или даже минуты. Система проверки типов даёт программисту мгновенную обратную связь и позволяет ему быстрее делать своё дело. Ему не нужно больше писать хрупкие и тяжёлые в поддержке модульные тесты, которые заменяют реальные сущности моками и патчами только ради того, чтобы быстрее получить результаты испытаний кода.
IDE и редакторы, такие, как PyCharm или Visual Studio Code, используют возможности аннотаций типов для предоставления разработчикам возможностей по автоматическому завершению кода, по подсветке ошибок, по поддержке часто используемых языковых конструкций. И это — лишь некоторые из плюсов, которые даёт типизация. Для некоторых программистов всё это — главный аргумент в пользу типизации. Это то, что приносит пользу сразу же после внедрения в работу. Этот вариант использования типов не требует применения отдельной системы проверки типов, такой, как mypy, хотя надо отметить, что mypy помогает поддерживать соответствие аннотаций типов и кода.
Предыстория mypy
История mypy началась в Великобритании, в Кембридже, за несколько лет до того, как я присоединился к Dropbox. Я занимался, в рамках проведения докторского исследования, вопросом унификации статически типизированных и динамических языков. Меня вдохновляла статья о постепенной типизации Джереми Сиека и Валида Таха, а так же проект Typed Racket. Я пытался найти способы использования одного и того же языка программирования для различных проектов — от маленьких скриптов, до кодовых баз, состоящих из многих миллионов строк. При этом мне хотелось, чтобы в проекте любого масштаба не пришлось бы идти на слишком большие компромиссы. Важной частью всего этого была идея о постепенном переходе от нетипизированного прототипа проекта к всесторонне протестированному статически типизированному готовому продукту. В наши дни эти идеи, в значительной степени, принимаются как должное, но в 2010 году это была проблема, которую всё ещё активно исследовали.
Моя изначальная работа в области проверки типов не была нацелена на Python. Вместо него я использовал маленький «самодельный» язык Alore. Вот пример, который позволит вам понять — о чём идёт речь (аннотации типов здесь необязательны):
def Fib(n as Int) as Int
if n <= 1
return n
else
return Fib(n - 1) + Fib(n - 2)
end
end
Использование упрощённого языка собственной разработки — это обычный подход, применяемый в научных исследованиях. Это так не в последнюю очередь из-за того, что подобное позволяет быстро проводить эксперименты, а также из-за того, что то, что к исследованию отношения не имеет, можно беспрепятственно игнорировать. Реально используемые языки программирования обычно представляют собой масштабные явления со сложными реализациями, а это эксперименты замедляет. Однако любые результаты, основанные на упрощённом языке, выглядят немного подозрительными, так как при получении этих результатов исследователь, может быть, пожертвовал соображениями, важными для практического использования языков.
Моё средство проверки типов для Alore выглядело весьма многообещающим, но мне хотелось проверить его, выполнив эксперименты с реальным кодом, которого, можно сказать, на Alore написано не было. К моему счастью, язык Alore в значительной степени был основан на тех же идеях, что и Python. Было достаточно просто переделать средство для проверки типов так, чтобы оно могло бы работать с синтаксисом и семантикой Python. Это позволило попробовать выполнить проверку типов в опенсорсном Python-коде. Кроме того, я написал транспайлер для преобразования кода, написанного на Alore в Python-код и использовал его для трансляции кода моего средства для проверки типов. Теперь у меня была система для проверки типов, написанная на Python, которая поддерживала подмножество Python, некую разновидность этого языка! (Определённые архитектурные решения, которые имели смысл для Alore, плохо подходили для Python, это всё ещё заметно в некоторых частях кодовой базы mypy.)
На самом деле, язык, поддерживаемый моей системой типов, в этот момент не вполне можно было назвать Python: это был вариант Python из-за некоторых ограничений синтаксиса аннотаций типов Python 3.
Выглядело это как смесь Java и Python:
int fib(int n):
if n <= 1:
return n
else:
return fib(n - 1) + fib(n - 2)
Одна из моих идей в то время заключалась в том, чтобы использовать аннотации типов для улучшения производительности путём компиляции этой разновидности Python в C, или, возможно, в байт-код JVM. Я продвинулся до стадии написания прототипа компилятора, но оставил эту затею, так как проверка типов и сама по себе выглядела достаточно полезной.
Я, в итоге, представил мой проект на конференции PyCon 2013 в Санта-Кларе. Так же я поговорил об этом с Гвидо ван Россумом, с великодушным пожизненным диктатором Python. Он убедил меня отказаться от собственного синтаксиса и придерживаться стандартного синтаксиса Python 3. Python 3 поддерживает аннотации функций, в результате мой пример можно было переписать так, как показано ниже, получив нормальную Python-программу:
def fib(n: int) -> int:
if n <= 1:
return n
else:
return fib(n - 1) + fib(n - 2)
Мне понадобилось пойти на некоторые компромиссы (в первую очередь хочу отметить, что я изобрёл собственный синтаксис именно поэтому). В частности, Python 3.3, самая свежая версия языка на тот момент, не поддерживал аннотаций переменных. Я обсудил с Гвидо по электронной почте различные возможности синтаксического оформления подобных аннотаций. Мы решили использовать для переменных комментарии с указанием типов. Это позволяло добиться поставленной цели, но выглядело несколько громоздко (Python 3.6 дал нам более приятный синтаксис):
products = [] # type: List[str] # Eww
Комментарии с типами, кроме того, пригодились для поддержки Python 2, в котором нет встроенной поддержки аннотаций типов:
f fib(n):
# type: (int) -> int
if n <= 1:
return n
else:
return fib(n - 1) + fib(n - 2)
Оказалось, что эти (и другие) компромиссы, на самом деле, не имели особого значения — преимущества статической типизации привели к тому, что пользователи скоро забыли о не вполне идеальном синтаксисе. Так как теперь в Python-коде, в котором контролировались типы, не применялись особые синтаксические конструкции, существующие Python-инструменты и процессы по обработке кода продолжили нормально работать, что значительно облегчило освоение разработчиками нового инструмента.
Гвидо, кроме того, убедил меня присоединиться к Dropbox после того, как я защитил выпускную работу. Тут начинается самое интересное в истории mypy.
Продолжение следует…
Уважаемые читатели! Если вы пользуетесь Python — просим рассказать о том, проекты какого масштаба вы разрабатываете на этом языке.
Автор: ru_vds