Tee 4 miljoni Pythoni koodirea tüübikontrolliks. 1. osa

Täna juhime teie tähelepanu materjali tõlke esimesele osale selle kohta, kuidas Dropbox tegeleb Pythoni koodi tüübikontrolliga.

Tee 4 miljoni Pythoni koodirea tüübikontrolliks. 1. osa

Dropbox kirjutab Pythonis palju. See on keel, mida kasutame äärmiselt laialdaselt nii taustateenuste kui ka töölauakliendi rakenduste jaoks. Samuti kasutame palju Go, TypeScripti ja Rusti, kuid Python on meie põhikeel. Arvestades meie ulatust ja me räägime miljonitest Pythoni koodi ridadest, selgus, et sellise koodi dünaamiline tippimine muutis asjatult selle mõistmise keeruliseks ja hakkas tõsiselt mõjutama tööviljakust. Selle probleemi leevendamiseks oleme hakanud oma koodi järk-järgult üle viima staatilisele tüübikontrollile, kasutades mypyt. See on Pythoni jaoks tõenäoliselt kõige populaarsem eraldiseisev tüübikontrollisüsteem. Mypy on avatud lähtekoodiga projekt, selle peamised arendajad töötavad Dropboxis.

Dropbox oli üks esimesi ettevõtteid, kes rakendas Pythoni koodis sellises mahus staatilist tüübikontrolli. Mypyt kasutatakse tänapäeval tuhandetes projektides. Seda tööriista lugematuid kordi, nagu öeldakse, "lahingus testitud". Oleme läbinud pika tee, et jõuda selleni, kus me praegu oleme. Teel oli palju ebaõnnestunud ettevõtmisi ja ebaõnnestunud katseid. See postitus hõlmab Pythonis staatilise tüübikontrolli ajalugu alates selle kivisest algusest minu uurimisprojekti osana kuni tänapäevani, mil tüübikontroll ja tüübivihjed on muutunud igapäevaseks lugematute Pythonis kirjutavate arendajate jaoks. Neid mehhanisme toetavad nüüd paljud tööriistad, nagu IDE-d ja koodianalüsaatorid.

Lugege teist osa

Miks on tüübikontroll vajalik?

Kui olete kunagi kasutanud dünaamiliselt sisestatud Pythonit, võib teil olla segadust, miks on viimasel ajal staatilise tippimise ja müpyga seotud segadust tekitanud. Või äkki meeldib teile Python just selle dünaamilise tippimise tõttu ja toimuv ajab teid lihtsalt häirima. Staatilise tippimise väärtuse võti on lahenduste ulatus: mida suurem on teie projekt, seda rohkem kaldute staatilisele tippimisele ja lõpuks, seda rohkem te seda tegelikult vajate.

Oletame, et teatud projekt on jõudnud kümnete tuhandete ridade suuruseks ja selgus, et selle kallal töötab mitu programmeerijat. Vaadates sarnast projekti, võime oma kogemuste põhjal öelda, et selle koodi mõistmine on arendajate produktiivsuse hoidmise võti. Ilma tüübimärkusteta võib olla keeruline välja mõelda, milliseid argumente funktsioonile edastada või milliseid tüüpe funktsioon tagastada saab. Siin on tüüpilised küsimused, millele on sageli raske vastata ilma tüübimärkusi kasutamata.

  • Kas see funktsioon saab tagastada None?
  • Mis see argument peaks olema? items?
  • Mis on atribuudi tüüp id: int Kas see on, str, või äkki mõni kohandatud tüüp?
  • Kas see argument peaks olema loetelu? Kas sellele on võimalik korrust edasi anda?

Kui vaatate järgmist tüübimärkusega koodilõiku ja proovite vastata sarnastele küsimustele, selgub, et see on kõige lihtsam ülesanne:

class Resource:
    id: bytes
    ...
    def read_metadata(self, 
                      items: Sequence[str]) -> Dict[str, MetadataItem]:
        ...

  • read_metadata tagasi ei tule None, kuna tagastustüüp ei ole Optional[…].
  • argument items on ridade jada. Seda ei saa juhuslikult korrata.
  • Atribuut id on baitide jada.

Ideaalses maailmas võiks eeldada, et kõiki selliseid peensusi kirjeldatakse sisseehitatud dokumentatsioonis (docstring). Kuid kogemused annavad palju näiteid selle kohta, et sellist dokumentatsiooni sageli koodis, millega peate töötama, ei järgita. Isegi kui selline dokumentatsioon on koodis olemas, ei saa loota selle absoluutsele õigsusele. See dokumentatsioon võib olla ebamäärane, ebatäpne ja põhjustada arusaamatusi. Suurtes meeskondades või suurtes projektides võib see probleem muutuda äärmiselt teravaks.

Kui Python paistab silma projektide alg- või vahefaasis, siis ühel hetkel võivad edukad projektid ja Pythonit kasutavad ettevõtted seista silmitsi elulise küsimusega: "Kas me peaksime kõik staatiliselt trükitud keeles ümber kirjutama?".

Tüübikontrollisüsteemid, nagu mypy, lahendavad ülaltoodud probleemi, pakkudes arendajale tüüpide kirjeldamiseks ametlikku keelt ja kontrollides, kas tüübideklaratsioonid vastavad programmi rakendamisele (ja valikuliselt kontrollides nende olemasolu). Üldiselt võib öelda, et need süsteemid annavad meie käsutusse midagi nagu hoolikalt kontrollitud dokumentatsiooni.

Selliste süsteemide kasutamisel on muid eeliseid ja need on juba täiesti ebaolulised:

  • Tüübikontrollisüsteem suudab tuvastada mõned väikesed (ja mitte nii väikesed) vead. Tüüpiline näide on see, kui nad unustavad väärtust töödelda None või mõni muu eritingimus.
  • Koodi ümberkujundamine on oluliselt lihtsustatud, kuna tüübikontrollisüsteem on sageli väga täpne selle kohta, millist koodi tuleb muuta. Samal ajal ei pea me lootma 100% koodikattele testidega, mis igal juhul pole tavaliselt teostatav. Probleemi põhjuse väljaselgitamiseks ei pea me süvenema virnajälje sügavustesse.
  • Isegi suurte projektide puhul suudab mypy sageli täieliku tüübikontrolli teha sekundi murdosaga. Ja testide täitmine võtab tavaliselt kümneid sekundeid või isegi minuteid. Tüübikontrollisüsteem annab programmeerijale kohese tagasiside ja võimaldab tal oma tööd kiiremini teha. Ta ei pea enam kirjutama hapraid ja raskesti hooldatavaid ühikuteste, mis asendavad tegelikud olemid pilkade ja paikadega, et kooditesti tulemusi kiiremini saada.

IDE-d ja redaktorid, nagu PyCharm või Visual Studio Code, kasutavad tüübimärkuste jõudu, et pakkuda arendajatele koodi lõpetamist, vigade esiletõstmist ja tavaliselt kasutatavate keelekonstruktsioonide tuge. Ja need on vaid mõned tippimise eelised. Mõne programmeerija jaoks on see kõik peamine argument trükkimise kasuks. Sellest on kasu kohe pärast rakendamist. See tüüpide kasutusjuhtum ei nõua eraldi tüübikontrollisüsteemi (nt mypy), kuigi tuleb märkida, et mypy aitab hoida tüübimärkused koodiga kooskõlas.

Mypy taust

Mypy ajalugu algas Ühendkuningriigis, Cambridge'is, paar aastat enne minu Dropboxiga liitumist. Olen oma doktoritöö raames tegelenud staatiliselt tipitud ja dünaamiliste keelte ühendamisega. Mind inspireerisid Jeremy Sieki ja Walid Taha artikkel järkjärgulise tippimise kohta ning projekt Typed Racket. Püüdsin leida võimalusi, kuidas kasutada sama programmeerimiskeelt erinevate projektide jaoks – alates väikestest skriptidest kuni paljudest miljonitest ridadest koosnevate koodibaasideni. Samas tahtsin tagada, et igasuguse mastaabiga projekti puhul ei peaks tegema liiga suuri kompromisse. Selle kõige oluliseks osaks oli idee liikuda järk-järgult tüpiseerimata prototüübiprojektilt igakülgselt testitud staatiliselt trükitud valmistootele. Tänapäeval peetakse neid ideid suures osas iseenesestmõistetavaks, kuid 2010. aastal oli see probleem, mida veel aktiivselt uuriti.

Minu algne töö tüübikontrolli alal ei olnud Pythonile suunatud. Selle asemel kasutasin väikest "omatehtud" keelt Alore. Siin on näide, mis aitab teil mõista, millest me räägime (tüüpmärkused on siin valikulised):

def Fib(n as Int) as Int
  if n <= 1
    return n
  else
    return Fib(n - 1) + Fib(n - 2)
  end
end

Lihtsustatud emakeele kasutamine on teadusuuringutes levinud lähenemisviis. See on nii, muu hulgas seetõttu, et see võimaldab teil kiiresti katseid läbi viia, ja ka seetõttu, et seda, millel pole uurimistööga mingit pistmist, saab kergesti ignoreerida. Reaalmaailma programmeerimiskeeled kipuvad olema keerukate rakendustega suuremahulised nähtused ja see aeglustab katsetamist. Kuid kõik lihtsustatud keelel põhinevad tulemused tunduvad pisut kahtlased, kuna nende tulemuste saamisel võis uurija ohverdada keele praktilise kasutamise seisukohalt olulised kaalutlused.

Minu tüübikontroll Alore jaoks tundus väga paljulubav, kuid tahtsin seda testida, katsetades päris koodiga, mis, võib öelda, polnud Alores kirjutatud. Minu õnneks põhines Alore keel suuresti samadel ideedel kui Python. Tüübikontrollijat oli piisavalt lihtne muuta, et see töötaks Pythoni süntaksi ja semantikaga. See võimaldas meil proovida tüübikontrolli avatud lähtekoodiga Pythoni koodis. Lisaks kirjutasin transpileri Alore'is kirjutatud koodi Pythoni koodiks teisendamiseks ja kasutasin seda oma typecheckeri koodi tõlkimiseks. Nüüd oli mul Pythonis kirjutatud tüübikontrollisüsteem, mis toetas Pythoni alamhulka, mingisugust seda keelt! (Teatud arhitektuurilised otsused, mis olid Alore jaoks mõistlikud, sobisid Pythoni jaoks halvasti ja see on müpy koodibaasi osades endiselt märgatav.)

Tegelikult ei saanud minu tüübisüsteemi toetatud keelt praegu päris Pythoniks nimetada: see oli Pythoni variant Python 3 tüüpi annotatsiooni süntaksi mõningate piirangute tõttu.

See nägi välja nagu Java ja Pythoni segu:

int fib(int n):
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

Üks minu toonane idee oli kasutada jõudluse parandamiseks tüübimärkusi, kompileerides seda tüüpi Pythoni C-ks või võib-olla JVM-i baitkoodiks. Jõudsin kompilaatori prototüübi kirjutamise faasi, kuid loobusin sellest ideest, kuna tüübikontroll ise tundus üsna kasulik.

Lõpuks esitlesin oma projekti PyCon 2013-s Santa Claras. Rääkisin sellest ka Guido van Rossumiga, heatahtliku Pythoni diktaatoriga kogu eluks. Ta veenis mind loobuma oma süntaksist ja jääma standardse Python 3 süntaksi juurde. Python 3 toetab funktsioonide annotatsioone, nii et minu näite saab ümber kirjutada, nagu allpool näidatud, mille tulemuseks on tavaline Pythoni programm:

def fib(n: int) -> int:
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

Pidin tegema mõningaid kompromisse (kõigepealt tahan märkida, et just sel põhjusel leiutasin oma süntaksi). Eelkõige ei toetanud Python 3.3, selle keele tolleaegne uusim versioon, muutujamärkusi. Arutasin Guidoga meili teel erinevaid võimalusi selliste annotatsioonide süntaktiliseks kujundamiseks. Otsustasime muutujate jaoks kasutada tüübikommentaare. See täitis ettenähtud eesmärki, kuid oli mõnevõrra tülikas (Python 3.6 andis meile ilusama süntaksi):

products = []  # type: List[str]  # Eww

Tüübikommentaarid olid kasulikud ka Python 2 toetamiseks, millel puudub sisseehitatud tüübimärkuste tugi:

f fib(n):
    # type: (int) -> int
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

Selgus, et nendel (ja teistel) kompromissidel polnudki tegelikult tähtsust – staatilise tippimise eelised tähendasid, et kasutajad unustasid peagi vähem kui täiusliku süntaksi. Kuna tüübikontrolliga Pythoni koodis ei kasutatud spetsiaalseid süntaktilisi konstruktsioone, jätkasid olemasolevad Pythoni tööriistad ja kooditöötlusprotsessid normaalselt töötamist, muutes arendajatel uue tööriista õppimise palju lihtsamaks.

Guido veenis mind ka pärast lõputöö valmimist Dropboxiga liituma. Siit algab müpiloo kõige huvitavam osa.

Jätkub ...

Kallid lugejad! Kui kasutate Pythonit, rääkige meile selles keeles arendatavate projektide mahust.

Tee 4 miljoni Pythoni koodirea tüübikontrolliks. 1. osa
Tee 4 miljoni Pythoni koodirea tüübikontrolliks. 1. osa

Allikas: www.habr.com

Lisa kommentaar