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

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

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

Читати першу частину

Офіційна підтримка типів (PEP 484)

Ми провели перші серйозні експерименти з mypy у Dropbox під час Hack Week 2014. Hack Week – це захід, який проводить Dropbox протягом одного тижня. У цей час співробітники можуть працювати над будь-чим! Деякі з найзнаменитіших технологічних проектів Dropbox розпочиналися саме на подібних заходах. В результаті цього експерименту ми зробили висновки про те, що mypy виглядає багатообіцяюче, хоча цей проект ще не був готовий для широкого використання.

У повітрі витала ідея про стандартизацію систем видачі підказок за типами Python. Як я вже казав, починаючи з Python 3.0 можна було користуватися анотаціями типів для функцій, але це були лише довільні вирази, без певних синтаксису і семантики. Під час виконання програми ці інструкції здебільшого просто ігнорувалися. Після Hack Week ми почали працювати над стандартизацією семантики. Ця робота призвела до появи Пеп 484 (Над цим документом спільно працювали Гвідо ван Россум, Лукаш Ланга і я).

Наші мотиви можна було розглядати із двох сторін. По-перше, ми сподівалися, що вся екосистема Python могла б прийняти загальний підхід до використання підказок типів (type hints — термін, який використовується в Python як аналог «анотацій типів»). Це, враховуючи можливі ризики, було краще, ніж використання безлічі взаємно несумісних підходів. По-друге, ми хотіли відкрито обговорити механізми анотування типів з багатьма представниками Python-спільноти. Почасти це бажання було продиктовано тим, що нам не хотілося б виглядати відступниками від базових ідей мови в очах широких мас Python-програмістів. Це динамічно типізована мова, відома «качиною типізацією». У суспільстві, на початку, не могло не виникнути дещо підозріле ставлення до ідеї статичної типізації. Але подібний настрій зрештою ослаб — після того, як стало зрозуміло, що статичну типізацію не планується робити обов'язковою (і після того, як люди зрозуміли, що вона по-справжньому корисна).

Прийнятий в результаті синтаксис підказок за типами був дуже схожий на той, що підтримував тоді mypy. Документ PEP 484 вийшов разом із Python 3.5 у 2015 році. Python більше не був мовою, яка підтримувала лише динамічну типізацію. Мені подобається думати про цю подію як про значну віху в історії Python.

Початок міграції

Наприкінці 2015 року в Dropbox для роботи над mypy була створена команда з трьох осіб. Туди входили Гвідо ван Россум, Грег Прайс та Девід Фішер. З цього моменту ситуація почала розвиватися дуже швидко. Першою перешкодою на шляху зростання mypy стала продуктивність. Як я вже натякав вище, в ранній період розвитку проекту я міркував про те, щоб перекласти реалізацію mypy мовою C, але ця ідея була поки що викреслена зі списків. Ми застрягли на тому, що для запуску системи використовувався інтерпретатор CPython, який не відрізняється швидкістю, достатньою для інструментів на зразок mypy. (Проект PyPy, альтернативна реалізація Python із JIT-компілятором, теж нам не допоміг.)

На щастя, тут нам на допомогу прийшли деякі алгоритмічні вдосконалення. Першим потужним прискорювачем стала реалізація інкрементної перевірки. Ідея цього поліпшення була проста: якщо всі залежності модуля з попереднього запуску mypy не змінилися, ми можемо використовувати, під час роботи з залежностями, дані, кешовані під час попереднього сеансу роботи. Нам потрібно було лише виконати перевірку типів у змінених файлах і тих файлах, які від них залежали. Mypy пішов трохи далі: якщо зовнішній інтерфейс модуля не змінювався — mypy вважав, що інші модулі, які імпортують цей модуль, не потрібно перевіряти повторно.

Інкрементна перевірка дуже допомогла нам при анотуванні великих обсягів існуючого коду. Справа в тому, що цей процес зазвичай включає безліч ітеративних запусків mypy, так як анотації поступово додають в код і поступово покращують. Перший запуск mypy все ще був дуже повільним, тому що при його виконанні потрібно було перевірити багато залежностей. Тоді ми, задля покращення ситуації, реалізували механізм віддаленого кешування. Якщо mypy виявляє, що локальний кеш, ймовірно, застарів, він завантажує поточний знімок кешу для всієї кодової бази централізованого репозиторію. Потім він виконує з використанням цього знімку інкрементальну перевірку. Це ще на один великий крок просунуло нас на шляху збільшення продуктивності mypy.

Це був період швидкого та природного впровадження системи перевірки типів у Dropbox. У нас, до кінця 2016 року, було вже приблизно 420000 XNUMX рядків Python-коду з анотаціями типів. Багато користувачів з ентузіазмом поставилися до перевірки типів. У Dropbox mypy користувалися дедалі більше команд розробників.

Все тоді виглядало добре, але нам ще багато треба було зробити. Ми почали виконувати періодичні внутрішні опитування користувачів для того, щоб виявити проблемні місця проекту та зрозуміти те, які питання потрібно вирішити насамперед (ця практика використовується в компанії та в наші дні). Найважливішими, як стало зрозуміло, виявилося два завдання. Перша - потрібно було більше покриття коду типами, друга - потрібно було, щоб mypy працював швидше. Цілком ясно було те, що наша робота з прискорення mypy і його впровадження в проекти компанії була ще далека від завершення. Ми повною мірою усвідомлюючи важливість цих двох завдань взялися за їх вирішення.

Більше продуктивності!

Інкрементні перевірки прискорили mypy, але цей інструмент ще не був досить швидким. Багато інкрементних перевірок тривали близько хвилини. Причиною такого були циклічні імпорти. Це, мабуть, не здивує того, кому доводилося працювати з великими кодовими базами, написаними на Python. У нас були набори із сотень модулів, кожен із яких побічно імпортував усі інші. Якщо будь-який файл у циклі імпортів виявлявся зміненим, mypy доводилося обробляти всі файли, що входять до цього циклу, а часто ще й будь-які модулі, що імпортують модулі з цього циклу. Одним з таких циклів був сумно відомий «клубок залежностей», який став причиною безлічі неприємностей у Dropbox. Одного разу ця структура містила кілька сотень модулів, при цьому її імпортували, прямо або непрямо, безліч тестів, вона використовувалася і в продакшн-коді.

Ми розглядали можливість «розплутування» циклічних залежностей, але ми не мали ресурсів для того, щоб це зробити. Там було надто багато коду, з яким ми не були знайомі. У результаті ми вийшли альтернативний підхід. Ми вирішили зробити так, щоб mypy працював би швидко навіть за наявності «клубків залежностей». Ми досягли цієї мети за допомогою демона mypy. Демон це серверний процес, який реалізує дві цікаві можливості. По-перше, він тримає в пам'яті інформацію про всю кодову базу. Це означає, що при кожному запуску mypy не потрібно завантажувати кешовані дані, що стосуються тисяч імпортованих залежностей. По-друге, він ретельно, на рівні дрібних структурних одиниць, аналізує залежності між функціями та іншими сутностями. Наприклад, якщо функція foo викликає функцію bar, то є залежність foo від bar. Коли змінюється файл - демон спочатку, в ізоляції, обробляє файл, що тільки змінився. Потім він дивиться на зміни цього файлу, видимі ззовні, на такі, як сигнатури функцій, що змінилися. Демон використовує детальну інформацію про імпорти лише для перевірки тих функцій, які по-справжньому використовують змінену функцію. Зазвичай за такого підходу перевіряти доводиться зовсім небагато функцій.

Реалізація всього цього була складною справою, тому що вихідна реалізація mypy була сильно орієнтована на обробку одного файлу за раз. Нам довелося зіткнутися з безліччю прикордонних ситуацій, виникнення яких вимагало повторних перевірок у випадках, коли у коді щось змінювалося. Наприклад, таке відбувається тоді, коли класу призначають новий базовий клас. Після того, як ми зробили те, що хотіли, ми змогли знизити час виконання більшості інкрементних перевірок за кілька секунд. Нам це здавалося великою перемогою.

Ще більше продуктивності!

Разом з віддаленим кешуванням, про який я розповідав вище, демон mypy практично повністю вирішив проблеми, що виникають тоді, коли програміст часто запускає перевірку типів, вносячи зміни до невеликої кількості файлів. Однак продуктивність системи в найменш сприятливому варіанті її використання все ще далека від оптимальної. Чистий запуск mypy міг зайняти понад 15 хвилин. А це було набагато більше, ніж нас влаштувало б. Щотижня ситуація ставала дедалі гіршою, оскільки програмісти продовжували писати новий код і додавати анотації до існуючого коду. Наші користувачі все ще прагнули більшої продуктивності, ми ж із задоволенням були готові піти їм назустріч.

Ми вирішили повернутися до однієї з ранніх ідей щодо mypy. А саме – до перетворення Python-коду на C-код. Експерименти з Cython (це система, яка дозволяє транслювати код, написаний на Python, в C-код) не дали нам якогось видимого прискорення, тому ми вирішили відродити ідею написання власного компілятора. Оскільки кодова база mypy (написана на Python) вже містила у собі всі необхідні інструкції типів, нам здавалося спроба використовувати ці інструкції для прискорення роботи системи. Я швидко створив макет для перевірки цієї ідеї. Він показав на різних мікробенчмарках більш ніж 10-кратне зростання продуктивності. Наша ідея полягала в тому, щоб компілювати Python-модулі в C-модулі засобами Cython, і в тому, щоб перетворювати анотації типів на перевірки типів, що виробляються під час виконання програми (зазвичай анотації типів ігноруються під час виконання програм і використовуються лише системами перевірки типів ). Ми фактично планували перекласти реалізацію mypy з Python на мову, яка була створена статично типізованою, яка б виглядала (і, здебільшого, працювала б) точно так, як Python. (Цей різновид міжмовної міграції став чимось подібним до традиції проекту mypy. Початкова реалізація mypy була написана на Alore, потім був синтаксичний гібрид Java і Python).

Орієнтація на API розширень CPython була ключем до того, щоб не втратити можливостей управління проектом. Нам не потрібно було реалізовувати віртуальну машину або будь-які бібліотеки, яких потребував mypy. Крім того, нам все ще була б доступна вся екосистема Python, були б доступні всі інструменти (такі як pytest). Це означало, що ми могли б продовжити використання інтерпретованого Python-коду в ході розробки, що дозволило б нам продовжити працювати, використовуючи дуже швидку схему внесення правок в код і його тестування, а не чекати компіляції коду. Виглядало це так, ніби нам чудово вдається, так би мовити, всидіти на двох стільцях, і нам це подобалося.

Компілятор, який ми назвали mypyc (оскільки він, як фронтенд, використовує для аналізу типів mypy), виявився проектом дуже успішним. Загалом ми досягли приблизно 4-кратного прискорення частих запусків mypy без використання кешування. Розробка ядра проекту mypyc зайняла у маленької команди, до якої входили Майкл Салліван, Іван Левківський, Х'ю Хан та я, близько 4 календарних місяців. Цей обсяг робіт був менш масштабним, ніж той, який би знадобився для переписування mypy, наприклад, на C++ або на Go. Та й змін до проекту нам довелося зробити набагато менше, ніж довелося б зробити при переписуванні його іншою мовою. Ми, крім того, сподівалися на те, що зможемо довести mypyc до такого рівня, щоб ним змогли б користуватися для компіляції та прискорення свого коду інші програмісти з Dropbox.

Для досягнення такого рівня продуктивності нам довелося застосувати деякі цікаві інженерні рішення. Так, компілятор може прискорити виконання багатьох операцій завдяки використанню швидких низькорівневих конструкцій C. Наприклад, виклик скомпільованої функції транслюється виклик C-функції. А такий виклик виконується набагато швидше за виклик інтерпретованої функції. Деякі операції, такі як пошук у словниках, все ще зводилися до використання звичайних викликів C-API з CPython, які після компіляції виявлялися лише трохи швидше. Ми змогли позбавитися додаткового навантаження на систему, що створюється інтерпретацією, але це в даному випадку дало лише невеликий виграш у плані продуктивності.

Для виявлення найпоширеніших «повільних» операцій ми виконали профіль коду. Озброєні отриманими даними, ми спробували або так «підкрутити» mypyc, щоб він генерував швидший C-код для подібних операцій, або переписати відповідний Python-код з використанням швидших операцій (а іноді у нас просто не було достатньо простого рішення для тієї операції чи іншої проблеми). Переписування Python-коду часто виявлялося легшим вирішенням проблеми, ніж реалізація автоматичного виконання тієї ж трансформації в компіляторі. У довгостроковій перспективі нам хотілося автоматизувати багато цих трансформацій, але в той момент ми були націлені на те, щоб, доклавши мінімум зусиль, прискорити mypy. І ми, рухаючись до цієї мети, зрізали кілька кутів.

Далі буде ...

Шановні читачі! Які враження у вас викликав проект mypy у той час, коли ви дізналися про його існування?

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

Джерело: habr.com

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