Сегодня мы предлагаем вашему вниманию первую часть перевода материала о том, как в 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 — просим рассказать о том, проекты какого масштаба вы разрабатываете на этом языке.


Комментарии (17)


  1. Groramar
    26.09.2019 18:13

    Типов и компиляции Питону очень не хватает. Я уже как-то писал по этому поводу:
    habr.com/ru/company/psb/blog/433434/#comment_19536944
    Да, можно что-то пробовать внешнее (mypy) в своем коде, однако это в существующем коде это сделать довольно сложно: код уже написан и просто для того, что бы разобраться как раз вот часто типов и не хватает.


    1. KvanTTT
      27.09.2019 00:47
      +1

      С типами и компиляцией Питон уже не будет Питоном. И не легче ли тогда уж будет перейти на язык с типами и компиляцией?


      1. Groramar
        27.09.2019 04:33

        1. worldmind
          27.09.2019 08:45

          При чём тут питон, это эффективные менеджеры протупили.


        1. evgenyk
          27.09.2019 13:55

          Я наверное так и не пойму две вещи в современном мире:
          1) Зачем внедрять статическую типизацию в Python. По-моему, это просто мода.
          2) Зачем нужно было делать Python3. Здесь, по-моему просто неуемное желание пихать все новые и новые фичи в ядро языка, вместо реализации в виде библиотек и фреймворков.


          1. DmitryKoterov
            28.09.2019 10:47

            1. Это не мода, после начала использования типов в популярном языке (js->ts, php->hack, python->mypy) уже через неделю назад вернуться невозможно эмоционально (при условии, что IDE не Блокнот). Безтиповой код начинает ощущаться эмоционально, как что-то очень мусорное. Попробуйте.
            2. Не потому ли Питон 3, что Питон чуть было не погиб из-за несовершенства менеджера пакетов (но, к счастью, перемахнул-таки долину смерти)? Просто гипотеза о связи.


      1. worldmind
        27.09.2019 08:44

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


        1. KvanTTT
          27.09.2019 09:31

          А чем мешают типы на стадии прототипа? Тем более в некоторых статических языках можно писать и без них (dynamic в C#).


          1. worldmind
            27.09.2019 09:38

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


            1. 0xd34df00d
              27.09.2019 14:56

              Мой субъективный опыт говорит, что и на стадии прототипа проще с типами.


    1. qellex
      27.09.2019 13:57

      Cython?


  1. DoubleW
    28.09.2019 02:58

    Ситуация с написанием миллионов строк на питон и попытка потом закостылить это сторонними решениями со стороны выглядит мягко говоря не очень.
    В целом хочется сказать всей этой индустрии (той самой где могут сначала написать миллионы строк на пхп/питоне, а потом пытаться решить это сделав собственный компилятор/стат анализатор/прочий костыль ломающий динамизм языка в угоду скорости/надежности).
    JUST STOP IT!


    1. DmitryKoterov
      28.09.2019 10:50

      Скажите это Фейсбуку с 20 ГБ кода на Hack-е с автовыкаткой в продакшен раз в час из монорепозитория Mercurial-а (гит уже не тянет такое). :) Кстати, к слову, кода очень приличного и по качеству, и по архитектуре.


      1. KvanTTT
        28.09.2019 11:52

        из монорепозитория Mercurial-а (гит уже не тянет такое)

        Хм, а меркурий тянет большие по размеру репы, чем гит? За счет чего, там же даже активней используется дельта-кодирование, если не ошибаюсь?


      1. DoubleW
        28.09.2019 13:21

        Мне это зачем говорить, они наверняка поняли это когда стали делать свой компилятор, своего подмножества пхп.
        Никто не спорит что эти костыли достигли промышленных масштабов — но вот зачем?
        Зачем авторы каждого нового проекта берут недоскриптовое однопоточное динамическое нечто и начинают потом приводить его костылями к большой тройке(C++, С#, Java).
        И даже в их условиях (тотальных микросервисов) они могут прекратить делать себе больно и начать использовать например тот же Go.


        1. qellex
          29.09.2019 01:12

          Зачем начинать делать себе больно с первой строчки кода если нет 100% уверенности что ваш код разростется до размеров, когда будет неважно на каком языке он написан и больно будет при любом раскладе?


    1. Vilaine
      29.09.2019 19:31

      Рынок труда и инфраструктура языка играют роль. Javascript, к примеру, неизбежен из-за первого (тяжело найти Typescript разработчиков). Мне кажется, это временная флуктуация в индустрии программирования немного вирусного характера.