4 milijonų Python kodo eilučių tipo tikrinimo kelias. 2 dalis

Šiandien skelbiame antrąją medžiagos apie tai, kaip Dropbox organizavo kelių milijonų Python kodo eilučių tipo valdymą, vertimo dalį.

4 milijonų Python kodo eilučių tipo tikrinimo kelias. 2 dalis

Skaitykite pirmą dalį

Oficialus tipo palaikymas (PEP 484)

Pirmuosius rimtus eksperimentus su mypy atlikome „Dropbox“ 2014 m. „Hack Week“ metu. „Hack Week“ yra vienos savaitės renginys, kurį organizuoja „Dropbox“. Per šį laiką darbuotojai gali dirbti ką nori! Kai kurie garsiausi „Dropbox“ technologijų projektai prasidėjo tokiuose renginiuose. Atlikę šį eksperimentą padarėme išvadą, kad „mypy“ atrodo daug žadanti, nors projektas dar nėra paruoštas plačiam naudojimui.

Tuo metu ore sklandė idėja standartizuoti Python tipo užuominų sistemas. Kaip jau sakiau, nuo Python 3.0 buvo galima naudoti tipo anotacijas funkcijoms, tačiau tai buvo tik savavališkos išraiškos, be apibrėžtos sintaksės ir semantikos. Vykdant programą šios anotacijos dažniausiai buvo tiesiog ignoruojamos. Po „Hack Week“ pradėjome dirbti su semantikos standartizavimu. Šis darbas paskatino atsiradimą PEP 484 (Guido van Rossum, Łukasz Langa ir aš bendradarbiavome rengdami šį dokumentą).

Mūsų motyvus galima būtų pažvelgti iš dviejų pusių. Pirma, mes tikėjomės, kad visa Python ekosistema gali priimti bendrą požiūrį į tipo užuominų naudojimą (terminas, naudojamas Python kaip „tipo anotacijų“ atitikmuo). Tai, atsižvelgiant į galimą riziką, būtų geriau nei daugelio tarpusavyje nesuderinamų metodų naudojimas. Antra, norėjome atvirai aptarti tipo anotacijos mechanizmus su daugeliu Python bendruomenės narių. Tokį norą iš dalies padiktavo tai, kad plačių Python programuotojų akyse nenorėtume atrodyti kaip „apostatai“ nuo pagrindinių kalbos idėjų. Tai dinamiškai spausdinama kalba, žinoma kaip „ančių spausdinimas“. Bendruomenėje pačioje pradžioje kilo šiek tiek įtartinas požiūris į statinio spausdinimo idėją. Tačiau šios nuotaikos galiausiai išblėso, kai paaiškėjo, kad statinis spausdinimas nebus privalomas (ir po to, kai žmonės suprato, kad tai iš tikrųjų naudinga).

Tipo užuominų sintaksė, kuri galiausiai buvo priimta, buvo labai panaši į tą, kurią mypy tuo metu palaikė. PEP 484 buvo išleistas kartu su Python 3.5 2015 m. Python nebebuvo dinamiškai spausdinama kalba. Man patinka galvoti apie šį įvykį kaip apie reikšmingą Python istorijos etapą.

Migracijos pradžia

2015 m. pabaigoje Dropbox sukūrė trijų žmonių komandą, kuri dirbs su mypy. Tarp jų buvo Guido van Rossum, Greg Price ir David Fisher. Nuo to momento situacija ėmė vystytis itin greitai. Pirmoji kliūtis mypy augimui buvo našumas. Kaip jau užsiminiau aukščiau, pirmosiomis projekto dienomis galvojau apie mypy diegimą išversti į C, tačiau ši idėja kol kas buvo išbraukta iš sąrašo. Mums įstrigo sistemos paleidimas naudojant CPython interpretatorių, kuris nėra pakankamai greitas tokiems įrankiams kaip mypy. (PyPy projektas, alternatyvus Python diegimas su JIT kompiliatoriumi, mums taip pat nepadėjo.)

Laimei, čia mums padėjo kai kurie algoritminiai patobulinimai. Pirmasis galingas „akceleratorius“ buvo laipsniško tikrinimo įgyvendinimas. Šio patobulinimo idėja buvo paprasta: jei visos modulio priklausomybės nepasikeitė nuo ankstesnio mypy paleidimo, tada dirbdami su priklausomybėmis galime naudoti duomenis, saugomus ankstesnio paleidimo metu. Mums tereikėjo atlikti modifikuotų failų ir nuo jų priklausančių failų tipo patikrą. Mypy netgi nuėjo šiek tiek toliau: jei išorinė modulio sąsaja nepasikeitė, mypy manė, kad kitų modulių, importavusių šį modulį, nereikia dar kartą tikrinti.

Laipsniškas tikrinimas mums labai padėjo komentuojant didelius esamo kodo kiekius. Esmė ta, kad šis procesas paprastai apima daug kartotinių mypy paleidimų, nes komentarai palaipsniui pridedami prie kodo ir palaipsniui tobulinami. Pirmasis mypy paleidimas vis dar buvo labai lėtas, nes reikėjo patikrinti daugybę priklausomybių. Tada, norėdami pagerinti situaciją, įdiegėme nuotolinio talpyklos mechanizmą. Jei „mypy“ nustato, kad vietinė talpykla greičiausiai pasenusi, ji iš centralizuotos saugyklos atsisiunčia visos kodų bazės dabartinę talpyklos momentinę kopiją. Tada jis atlieka laipsnišką patikrinimą naudodamas šį momentinį vaizdą. Tai žengė dar vieną didelį žingsnį link mypy našumo didinimo.

Tai buvo greito ir natūralaus tipo tikrinimo „Dropbox“ pritaikymo laikotarpis. 2016 m. pabaigoje jau turėjome maždaug 420000 XNUMX Python kodo eilučių su tipo anotacijomis. Daugelis vartotojų entuziastingai tikrino tipą. Vis daugiau kūrėjų komandų naudojo „Dropbox mypy“.

Tada viskas atrodė gerai, bet dar turėjome daug ką nuveikti. Pradėjome periodiškai vykdyti vidines vartotojų apklausas, siekdami nustatyti problemines projekto sritis ir suprasti, kokias problemas reikia išspręsti pirmiausia (tokia praktika įmonėje taikoma ir šiandien). Svarbiausios, kaip paaiškėjo, buvo dvi užduotys. Pirma, mums reikėjo didesnio tipo kodo aprėpties, antra, kad „mypy“ veiktų greičiau. Buvo visiškai aišku, kad mūsų darbas, siekiant paspartinti mypy ir įdiegti jį į įmonės projektus, dar toli gražu nebuvo baigtas. Mes, puikiai suvokdami šių dviejų užduočių svarbą, ėmėmės jas spręsti.

Daugiau produktyvumo!

Atliekant laipsniškus patikrinimus mypy buvo greitesnis, tačiau įrankis vis tiek nebuvo pakankamai greitas. Daugelis papildomų patikrinimų truko apie minutę. To priežastis – cikliškas importas. Tai tikriausiai nenustebins nė vieno, kuris dirbo su didelėmis kodų bazėmis, parašytomis Python. Turėjome šimtų modulių rinkinius, kurių kiekvienas netiesiogiai importavo visus kitus. Jei kuris nors importavimo ciklo failas buvo pakeistas, mypy turėjo apdoroti visus to ciklo failus ir dažnai visus modulius, importuojančius modulius iš tos kilpos. Vienas iš tokių ciklų buvo liūdnai pagarsėjęs „priklausomybės raizginys“, sukėlęs daug problemų „Dropbox“. Kai šioje struktūroje buvo keli šimtai modulių, ji buvo tiesiogiai ar netiesiogiai importuota, daug testų, ji taip pat buvo naudojama gamybos kode.

Svarstėme apie galimybę „atpainioti“ žiedines priklausomybes, bet neturėjome tam resursų. Buvo per daug kodo, su kuriuo mes nebuvome susipažinę. Dėl to mes sugalvojome alternatyvų požiūrį. Nusprendėme, kad mypy greitai veiktų net ir esant „priklausomybės raizginiams“. Šį tikslą pasiekėme naudodami „mypy“ demoną. Demonas yra serverio procesas, įgyvendinantis dvi įdomias funkcijas. Pirma, ji saugo informaciją apie visą kodų bazę atmintyje. Tai reiškia, kad kiekvieną kartą paleidus mypy, jums nereikia įkelti talpyklos duomenų, susijusių su tūkstančiais importuotų priklausomybių. Antra, jis atidžiai, mažų struktūrinių padalinių lygmeniu, analizuoja priklausomybes tarp funkcijų ir kitų subjektų. Pavyzdžiui, jei funkcija foo iškviečia funkciją bar, tada atsiranda priklausomybė foo nuo bar. Kai failas pasikeičia, demonas pirmiausia, atskirai, apdoroja tik pakeistą failą. Tada peržiūrimi išoriškai matomi to failo pakeitimai, pvz., pakeisti funkcijų parašai. Demonas naudoja išsamią informaciją apie importavimą tik tam, kad dar kartą patikrintų tas funkcijas, kurios iš tikrųjų naudoja pakeistą funkciją. Paprastai taikant šį metodą reikia patikrinti labai nedaug funkcijų.

Visa tai įgyvendinti nebuvo lengva, nes pradinis mypy diegimas buvo sutelktas į vieno failo apdorojimą vienu metu. Teko susidurti su daugybe ribinių situacijų, kurioms įvykus reikėjo pakartotinai tikrinti tais atvejais, kai kažkas pasikeitė kode. Pavyzdžiui, tai atsitinka, kai klasei priskiriama nauja bazinė klasė. Atlikę tai, ko norėjome, galėjome sutrumpinti daugumos papildomų patikrų vykdymo laiką iki kelių sekundžių. Mums tai atrodė didelė pergalė.

Dar didesnis produktyvumas!

Kartu su nuotoliniu talpyklos kaupimu, kurį aptariau aukščiau, mypy demonas beveik visiškai išsprendė problemas, kylančias, kai programuotojas dažnai vykdo tipo tikrinimą, pakeisdamas nedidelį skaičių failų. Tačiau sistemos veikimas nepalankiu naudojimo atveju vis dar buvo toli nuo optimalaus. Švarus mypy paleidimas gali užtrukti daugiau nei 15 minučių. Ir tai buvo daug daugiau, nei būtume patenkinti. Kiekvieną savaitę situacija darėsi vis blogesnė, nes programuotojai toliau rašė naują kodą ir papildė esamo kodo komentarus. Mūsų vartotojai vis dar troško didesnio našumo, bet džiaugiamės galėdami juos sutikti pusiaukelėje.

Nusprendėme grįžti prie vienos iš ankstesnių idėjų, susijusių su mypy. Būtent, konvertuoti Python kodą į C kodą. Eksperimentai su Cython (sistema, leidžiančia išversti Python parašytą kodą į C kodą) nepastebėjo jokio pastebimo greičio, todėl nusprendėme atgaivinti idėją parašyti savo kompiliatorių. Kadangi mypy kodų bazėje (parašyta Python) jau buvo visos reikalingos tipo anotacijos, pagalvojome, kad būtų verta pabandyti panaudoti šias anotacijas, kad sistema paspartintų. Greitai sukūriau prototipą, kad išbandyčiau šią idėją. Tai parodė daugiau nei 10 kartų didesnį našumą pagal įvairius mikro etalonus. Mūsų idėja buvo kompiliuoti Python modulius į C modulius naudojant Cython ir tipo komentarus paversti vykdymo laiko tipo patikra (paprastai tipo komentarai yra ignoruojami vykdymo metu ir naudojami tik tipo tikrinimo sistemose). Mes iš tikrųjų planavome išversti mypy įgyvendinimą iš Python į kalbą, kuri buvo sukurta statiškai įvesti, kuri atrodytų (ir dažniausiai veiktų) lygiai taip pat, kaip Python. (Toks tarpkalbių perkėlimas tapo tarsi mypy projekto tradicija. Originalus mypy diegimas buvo parašytas Alore, tada buvo sintaksinis Java ir Python hibridas).

Norint neprarasti projektų valdymo galimybių, labai svarbu sutelkti dėmesį į CPython plėtinio API. Mums nereikėjo įdiegti virtualios mašinos ar bibliotekų, kurių reikėjo mypy. Be to, vis tiek turėtume prieigą prie visos Python ekosistemos ir visų įrankių (pvz., pytest). Tai reiškė, kad kūrimo metu galėjome ir toliau naudoti interpretuotą Python kodą, o tai leido mums toliau dirbti su labai greitu kodo pakeitimų ir jo testavimo modeliu, o ne laukti, kol kodas bus sukompiliuotas. Atrodė, kad, taip sakant, sėdėdami ant dviejų kėdžių padarėme puikų darbą, ir mums tai patiko.

Kompiliatorius, kurį pavadinome mypyc (kadangi jis naudoja mypy kaip priekinę dalį tipų analizei), pasirodė esąs labai sėkmingas projektas. Apskritai, mes pasiekėme maždaug 4 kartus didesnį greitį, kai dažnai „mypy“ veikia be talpyklos. Mypyc projekto branduolio kūrimas užtruko apie 4 kalendorinius mėnesius mažai Michaelo Sullivano, Ivano Levkivsky, Hugh Hahno ir manęs. Šis darbo kiekis buvo daug mažesnis, nei būtų reikėję perrašyti mypy, pavyzdžiui, C++ arba Go. Ir mes turėjome padaryti daug mažiau projekto pakeitimų, nei būtume turėję padaryti jį perrašant kita kalba. Taip pat tikėjomės, kad „mypyc“ galėsime pasiekti tokį lygį, kad kiti „Dropbox“ programuotojai galėtų jį panaudoti savo kodui kompiliuoti ir pagreitinti.

Norėdami pasiekti tokį našumo lygį, turėjome pritaikyti keletą įdomių inžinerinių sprendimų. Taigi, kompiliatorius gali pagreitinti daugybę operacijų, naudodamas greitas, žemo lygio C konstrukcijas. Pavyzdžiui, sukompiliuotas funkcijos iškvietimas paverčiamas C funkcijos iškvietimu. Ir toks iškvietimas yra daug greitesnis nei interpretuotos funkcijos iškvietimas. Kai kurioms operacijoms, pvz., žodynų paieškai, vis dar naudojami įprasti C-API iškvietimai iš CPython, kurie buvo tik šiek tiek greitesni, kai buvo sudaryti. Mums pavyko pašalinti papildomą sistemos apkrovą, atsiradusią dėl interpretacijos, tačiau tai šiuo atveju davė tik nedidelį našumą.

Norėdami nustatyti dažniausiai pasitaikančias „lėtas“ operacijas, atlikome kodo profiliavimą. Apsiginklavę šiais duomenimis, bandėme arba pakoreguoti mypyc, kad jis generuotų greitesnį C kodą tokioms operacijoms, arba perrašyti atitinkamą Python kodą naudodami greitesnes operacijas (o kartais tiesiog neturėjome pakankamai paprasto sprendimo tai ar kitai problemai) . Python kodo perrašymas dažnai buvo lengvesnis problemos sprendimas nei kompiliatorius automatiškai atlikti tą pačią transformaciją. Ilgainiui norėjome automatizuoti daugelį šių transformacijų, tačiau tuo metu buvome sutelkę dėmesį į mypy paspartinimą su minimaliomis pastangomis. Ir judėdami šio tikslo link nukirtome kelis kampus.

Turi būti tęsiama ...

Mieli skaitytojai! Kokie buvo jūsų įspūdžiai apie „mypy“ projektą, kai sužinojote apie jo egzistavimą?

4 milijonų Python kodo eilučių tipo tikrinimo kelias. 2 dalis
4 milijonų Python kodo eilučių tipo tikrinimo kelias. 2 dalis

Šaltinis: www.habr.com

Добавить комментарий