Шлях да праверкі тыпаў 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 мы пачалі працаваць над стандартызацыяй семантыкі. Гэтая праца прывяла да з'яўлення PEP 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 радкоў 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-модулі ў С-модулі сродкамі 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

Дадаць каментар