Шлях до перевірки типів 4 мільйонів рядків Python-коду. Частина 1

Сьогодні ми пропонуємо до вашої уваги першу частину перекладу матеріалу про те, як у Dropbox займаються контролем типів Python-коду.

Шлях до перевірки типів 4 мільйонів рядків Python-коду. Частина 1

У 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 – просимо розповісти про те, проекти якого масштабу ви розробляєте цією мовою.

Шлях до перевірки типів 4 мільйонів рядків Python-коду. Частина 1
Шлях до перевірки типів 4 мільйонів рядків Python-коду. Частина 1

Джерело: habr.com

Додати коментар або відгук