Pot do preverjanja tipa 4 milijonov vrstic kode Python. 2. del

Danes objavljamo drugi del prevoda gradiva o tem, kako je Dropbox organiziral tipski nadzor za več milijonov vrstic kode Python.

Pot do preverjanja tipa 4 milijonov vrstic kode Python. 2. del

Preberi prvi del

Uradna tipska podpora (PEP 484)

Prve resne poskuse z mypy smo izvedli v Dropboxu med Hack Weekom 2014. Hack Week je enotedenski dogodek, ki ga gosti Dropbox. V tem času lahko zaposleni delajo, kar hočejo! Nekateri Dropboxovi najbolj znani tehnološki projekti so se začeli na takih dogodkih. Kot rezultat tega poskusa smo ugotovili, da mypy izgleda obetavno, čeprav projekt še ni pripravljen za široko uporabo.

Takrat je bila v zraku zamisel o standardizaciji sistemov namigovanja tipa Python. Kot sem rekel, je bilo od Pythona 3.0 mogoče uporabljati opombe tipov za funkcije, vendar so bili to samo poljubni izrazi, brez definirane sintakse in semantike. Med izvajanjem programa so bile te opombe večinoma preprosto prezrte. Po Hack Weeku smo začeli delati na standardizaciji semantike. To delo je privedlo do nastanka PEP 484 (Guido van Rossum, Łukasz Langa in jaz smo sodelovali pri tem dokumentu).

Na naše motive bi lahko gledali z dveh plati. Prvič, upali smo, da bo celoten ekosistem Python lahko sprejel skupen pristop k uporabi tipskih namigov (izraz, ki se v Pythonu uporablja kot enakovredno "opombam tipa"). To bi bilo glede na možna tveganja boljše od uporabe številnih med seboj nezdružljivih pristopov. Drugič, s številnimi člani skupnosti Python smo želeli odkrito razpravljati o mehanizmih označevanja tipa. To željo je deloma narekovalo dejstvo, da ne želimo v očeh širokih množic programerjev Pythona izgledati kot »odpadniki« od osnovnih idej jezika. Je dinamično tipkan jezik, znan kot "račje tipkanje". V skupnosti se na samem začetku ni mogel pojaviti nekoliko sumničav odnos do ideje o statičnem tipkanju. Toda to razpoloženje se je sčasoma zmanjšalo, ko je postalo jasno, da statično tipkanje ne bo obvezno (in ko so ljudje spoznali, da je dejansko uporabno).

Sintaksa tipskega namiga, ki je bila sčasoma sprejeta, je bila zelo podobna tisti, ki jo je takrat podpiral mypy. PEP 484 je bil izdan s Pythonom 3.5 leta 2015. Python ni bil več dinamično tipkan jezik. O tem dogodku rad razmišljam kot o pomembnem mejniku v zgodovini Pythona.

Začetek selitve

Konec leta 2015 je Dropbox ustvaril skupino treh ljudi za delo na mypy. Med njimi so bili Guido van Rossum, Greg Price in David Fisher. Od tistega trenutka naprej se je situacija začela izjemno hitro razvijati. Prva ovira za rast mypy je bila uspešnost. Kot sem namignil zgoraj, sem v zgodnjih dneh projekta razmišljal o prevodu implementacije mypy v C, vendar je bila ta ideja za zdaj črtana s seznama. Zataknili smo se pri izvajanju sistema s tolmačem CPython, ki ni dovolj hiter za orodja, kot je mypy. (Tudi projekt PyPy, alternativna implementacija Pythona s prevajalnikom JIT, nam ni pomagal.)

Na srečo so nam tukaj na pomoč priskočile nekatere algoritemske izboljšave. Prvi močan »pospeševalec« je bila implementacija inkrementalnega preverjanja. Zamisel za to izboljšavo je bila preprosta: če se vse odvisnosti modula niso spremenile od prejšnjega zagona mypy, potem lahko med delom z odvisnostmi uporabimo podatke, predpomnjene med prejšnjim zagonom. Izvesti smo morali samo preverjanje tipa spremenjenih datotek in datotek, ki so odvisne od njih. Mypy je šel celo nekoliko dlje: če se zunanji vmesnik modula ni spremenil, je mypy domneval, da drugih modulov, ki so uvozili ta modul, ni treba znova preverjati.

Postopno preverjanje nam je zelo pomagalo pri označevanju velikih količin obstoječe kode. Bistvo je, da ta postopek običajno vključuje veliko iterativnih zagonov mypy, saj se opombe postopoma dodajajo kodi in postopoma izboljšujejo. Prvi zagon mypy je bil še vedno zelo počasen, ker je bilo treba preveriti veliko odvisnosti. Nato smo za izboljšanje situacije implementirali mehanizem za oddaljeno predpomnjenje. Če mypy zazna, da je lokalni predpomnilnik verjetno zastarel, prenese trenutni posnetek predpomnilnika za celotno kodno zbirko iz centraliziranega repozitorija. Nato izvede postopno preverjanje s tem posnetkom. S tem smo naredili še en velik korak k povečanju zmogljivosti mypy.

To je bilo obdobje hitrega in naravnega sprejemanja preverjanja tipa v Dropboxu. Do konca leta 2016 smo imeli že približno 420000 vrstic kode Python z opombami tipa. Mnogi uporabniki so bili nad preverjanjem tipa navdušeni. Vse več razvojnih skupin je uporabljalo Dropbox mypy.

Takrat je vse kazalo dobro, a morali smo še veliko postoriti. Začeli smo izvajati občasne interne ankete med uporabniki, da bi prepoznali problematična področja projekta in razumeli, katera vprašanja je treba najprej rešiti (ta praksa se v podjetju uporablja še danes). Najpomembnejši sta bili, kot je postalo jasno, dve nalogi. Prvič, potrebovali smo več tipske pokritosti kode, drugič, potrebovali smo mypy za hitrejše delovanje. Popolnoma jasno je bilo, da naše delo pri pospešitvi mypy in njegovi implementaciji v projekte podjetja še zdaleč ni končano. Z zavedanjem pomena teh dveh nalog smo se lotili njunega reševanja.

Več produktivnosti!

S postopnimi pregledi je bil mypy hitrejši, vendar orodje še vedno ni bilo dovolj hitro. Številni postopni pregledi so trajali približno minuto. Razlog za to je bil cikličen uvoz. To verjetno ne bo presenetilo nikogar, ki je delal z velikimi kodnimi bazami, napisanimi v Pythonu. Imeli smo sklope na stotine modulov, od katerih je vsak posredno uvozil vse druge. Če je bila katera koli datoteka v uvozni zanki spremenjena, je moral mypy obdelati vse datoteke v tej zanki in pogosto vse module, ki so uvozili module iz te zanke. Eden takih ciklov je bil zloglasni "zaplet odvisnosti", ki je povzročil veliko težav pri Dropboxu. Ko je ta struktura vsebovala več sto modulov, medtem ko je bila neposredno ali posredno uvožena s številnimi testi, je bila uporabljena tudi v produkcijski kodi.

Razmišljali smo o možnosti "razpletanja" krožnih odvisnosti, vendar nismo imeli sredstev za to. Bilo je preveč kode, ki je nismo poznali. Posledično smo prišli do alternativnega pristopa. Odločili smo se, da bo mypy deloval hitro tudi v prisotnosti »zapletov odvisnosti«. Ta cilj smo dosegli z uporabo demona mypy. Daemon je strežniški proces, ki izvaja dve zanimivi funkciji. Prvič, v pomnilnik shrani informacije o celotni kodni bazi. To pomeni, da vam vsakič, ko zaženete mypy, ni treba naložiti predpomnjenih podatkov, povezanih s tisoči uvoženih odvisnosti. Drugič, skrbno, na ravni majhnih strukturnih enot, analizira odvisnosti med funkcijami in drugimi entitetami. Na primer, če funkcija foo pokliče funkcijo bar, potem obstaja odvisnost foo od bar. Ko se datoteka spremeni, demon najprej v izolaciji obdela samo spremenjeno datoteko. Nato pogleda zunanje vidne spremembe te datoteke, kot so spremenjeni podpisi funkcij. Demon uporablja podrobne informacije o uvozih samo za dvojno preverjanje tistih funkcij, ki dejansko uporabljajo spremenjeno funkcijo. Običajno morate s tem pristopom preveriti zelo malo funkcij.

Implementacija vsega tega ni bila enostavna, saj je bila prvotna implementacija mypy močno osredotočena na obdelavo ene datoteke naenkrat. Imeli smo opravka s številnimi mejnimi situacijami, katerih nastanek je zahteval večkratna preverjanja v primerih, ko se je kaj spremenilo v kodi. To se na primer zgodi, ko je razredu dodeljen nov osnovni razred. Ko smo naredili, kar smo želeli, smo lahko zmanjšali čas izvajanja večine inkrementalnih pregledov na le nekaj sekund. To se nam je zdela velika zmaga.

Še več produktivnosti!

Skupaj z oddaljenim predpomnjenjem, o katerem sem govoril zgoraj, je demon mypy skoraj popolnoma rešil težave, ki nastanejo, ko programer pogosto izvaja preverjanje tipa in spreminja majhno število datotek. Vendar je bilo delovanje sistema v najmanj ugodnem primeru uporabe še daleč od optimalnega. Čisti zagon mypy lahko traja več kot 15 minut. In to je bilo veliko več, kot bi bili zadovoljni. Vsak teden je bilo stanje slabše, saj so programerji še naprej pisali novo kodo in dodajali opombe obstoječi kodi. Naši uporabniki so bili še vedno lačni več zmogljivosti, vendar smo bili veseli, da smo jim ustregli na pol poti.

Odločili smo se, da se vrnemo k eni od prejšnjih idej glede mypy. In sicer za pretvorbo kode Python v kodo C. Eksperimentiranje s Cythonom (sistem, ki vam omogoča prevajanje kode, napisane v Pythonu, v kodo C) nam ni dalo nobene vidne pospešitve, zato smo se odločili oživiti idejo o pisanju lastnega prevajalnika. Ker je kodna zbirka mypy (napisana v Pythonu) že vsebovala vse potrebne opombe tipa, smo mislili, da bi bilo vredno poskusiti uporabiti te opombe za pospešitev sistema. Hitro sem ustvaril prototip, da bi preizkusil to idejo. Pokazal je več kot 10-kratno povečanje zmogljivosti na različnih mikromerilih. Naša ideja je bila prevesti module Python v module C z uporabo Cythona in spremeniti opombe tipa v preverjanje tipa med izvajanjem (običajno se opombe tipa med izvajanjem prezrejo in uporabljajo samo sistemi za preverjanje tipa). Pravzaprav smo načrtovali prevesti implementacijo mypy iz Pythona v jezik, ki je bil zasnovan za statično tipkanje, ki bi izgledal (in večinoma deloval) natanko tako kot Python. (Ta vrsta medjezikovne migracije je postala nekakšna tradicija projekta mypy. Prvotna izvedba mypy je bila napisana v Aloreju, potem je bil sintaktični hibrid Jave in Pythona).

Osredotočanje na razširitveni API CPython je bilo ključnega pomena, da ne izgubimo zmogljivosti upravljanja projektov. Ni nam bilo treba implementirati virtualnega stroja ali kakršnih koli knjižnic, ki jih je potreboval mypy. Poleg tega bi še vedno imeli dostop do celotnega ekosistema Python in vseh orodij (kot je pytest). To je pomenilo, da smo lahko med razvojem še naprej uporabljali interpretirano kodo Python, kar nam je omogočilo, da nadaljujemo z zelo hitrim vzorcem spreminjanja kode in njenega testiranja, namesto da čakamo, da se koda prevede. Videti je bilo, kot da sedimo na dveh stolih, tako rekoč odlično, in bilo nam je všeč.

Prevajalnik, ki smo ga poimenovali mypyc (ker uporablja mypy kot frontend za analizo tipov), se je izkazal za zelo uspešen projekt. Na splošno smo dosegli približno 4-kratno pospešitev za pogoste zagone mypy brez predpomnjenja. Za razvoj jedra projekta mypyc je majhna ekipa Michaela Sullivana, Ivana Levkivskyja, Hugha Hahna in mene porabila približno 4 koledarske mesece. Ta količina dela je bila veliko manjša od tiste, ki bi bila potrebna za ponovno pisanje mypy, na primer v C++ ali Go. In v projekt smo morali narediti veliko manj sprememb, kot bi jih morali narediti, ko bi ga prepisali v drugem jeziku. Upali smo tudi, da bomo lahko mypyc dvignili na takšno raven, da bi ga lahko drugi programerji Dropboxa uporabljali za prevajanje in pospešitev svoje kode.

Da bi dosegli to raven zmogljivosti, smo morali uporabiti nekaj zanimivih inženirskih rešitev. Tako lahko prevajalnik pospeši številne operacije z uporabo hitrih nizkonivojskih konstruktov C. Na primer, preveden klic funkcije se prevede v klic funkcije C. In tak klic je veliko hitrejši kot klic interpretirane funkcije. Nekatere operacije, kot je iskanje po slovarju, so še vedno vključevale uporabo običajnih klicev C-API iz CPythona, ki so bili pri prevajanju le nekoliko hitrejši. Uspelo nam je odpraviti dodatno obremenitev sistema, ki je nastala zaradi interpretacije, vendar je to v tem primeru dalo le majhen pribitek v smislu zmogljivosti.

Za identifikacijo najpogostejših »počasnih« operacij smo izvedli profiliranje kode. Oboroženi s temi podatki smo poskušali prilagoditi mypyc tako, da bi generiral hitrejšo kodo C za takšne operacije, ali pa prepisati ustrezno kodo Python s hitrejšimi operacijami (in včasih preprosto nismo imeli dovolj preproste rešitve za to ali drugo težavo) . Ponovno pisanje kode Python je bila pogosto lažja rešitev težave, kot če bi prevajalnik samodejno izvedel isto transformacijo. Dolgoročno smo želeli avtomatizirati veliko teh transformacij, vendar smo bili takrat osredotočeni na pospešitev mypy z minimalnim naporom. In pri premikanju proti temu cilju smo presekali več ovinkov.

Se nadaljuje ...

Drage bralke in bralci! Kakšni so bili vaši vtisi o projektu mypy, ko ste izvedeli za njegov obstoj?

Pot do preverjanja tipa 4 milijonov vrstic kode Python. 2. del
Pot do preverjanja tipa 4 milijonov vrstic kode Python. 2. del

Vir: www.habr.com

Dodaj komentar