Put do provjere tipa 4 miliona linija Python koda. Dio 1

Danas vam predstavljamo prvi dio prijevoda materijala o tome kako se Dropbox bavi kontrolom tipova Python koda.

Put do provjere tipa 4 miliona linija Python koda. Dio 1

Dropbox puno piše u Pythonu. To je jezik koji koristimo izuzetno široko, kako za pozadinske usluge tako i za desktop klijentske aplikacije. Takođe često koristimo Go, TypeScript i Rust, ali Python je naš glavni jezik. S obzirom na naše razmere, a radi se o milionima linija Python koda, pokazalo se da je dinamičko kucanje takvog koda nepotrebno zakomplikovalo njegovo razumevanje i počelo ozbiljno da utiče na produktivnost rada. Da bismo ublažili ovaj problem, počeli smo postupno prelaziti naš kod na statičku provjeru tipa koristeći mypy. Ovo je vjerovatno najpopularniji samostalni sistem za provjeru tipa za Python. Mypy je projekat otvorenog koda, njegovi glavni programeri rade u Dropboxu.

Dropbox je bila jedna od prvih kompanija koja je implementirala statičku provjeru tipova u Python kodu na ovom nivou. Mypy se ovih dana koristi u hiljadama projekata. Ovaj alat je bezbroj puta, kako kažu, "testiran u borbi". Prešli smo dug put da stignemo tamo gde smo sada. Na tom putu bilo je mnogo neuspješnih poduhvata i neuspjelih eksperimenata. Ovaj post pokriva historiju statičke provjere tipova u Pythonu, od njegovih kamenih početaka kao dijela mog istraživačkog projekta, do danas, kada su provjera tipa i nagoveštavanje tipova postali uobičajeni za bezbroj programera koji pišu na Pythonu. Ovi mehanizmi su sada podržani od strane mnogih alata kao što su IDE i analizatori koda.

Pročitajte drugi dio

Zašto je provjera tipa neophodna?

Ako ste ikada koristili dinamički kucani Python, možda ćete imati zabunu zašto se u posljednje vrijeme digla tolika gužva oko statičkog kucanja i mypyja. Ili možda volite Python upravo zbog njegovog dinamičkog kucanja, a ono što se dešava vas jednostavno uznemiri. Ključ vrijednosti statičkog kucanja je razmjer rješenja: što je vaš projekt veći, to više naginjete statičkom kucanju, i na kraju, to vam je više potrebno.

Pretpostavimo da je određeni projekat dostigao veličinu desetina hiljada linija, a ispostavilo se da nekoliko programera radi na njemu. Gledajući sličan projekat, na osnovu našeg iskustva, možemo reći da će razumijevanje njegovog koda biti ključ za održavanje produktivnosti programera. Bez napomena tipa može biti teško shvatiti, na primjer, koje argumente treba proslijediti funkciji ili koje tipove funkcija može vratiti. Evo tipičnih pitanja na koja je često teško odgovoriti bez upotrebe bilješki tipa:

  • Može li se ova funkcija vratiti None?
  • Šta bi ovaj argument trebao biti? items?
  • Koji je tip atributa id: int je li to, str, ili možda neki prilagođeni tip?
  • Da li ovaj argument treba da bude lista? Da li je moguće prenijeti tuple na njega?

Ako pogledate sljedeći isječak koda označenog tipom i pokušate odgovoriti na slična pitanja, ispostavit će se da je ovo najjednostavniji zadatak:

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

  • read_metadata se ne vraća None, budući da tip vraćanja nije Optional[…].
  • Argument items je niz linija. Ne može se nasumično ponavljati.
  • Atribut id je niz bajtova.

U idealnom svijetu, očekivalo bi se da će sve takve suptilnosti biti opisane u ugrađenoj dokumentaciji (docstring). Ali iskustvo daje mnogo primjera da se takva dokumentacija često ne poštuje u kodu s kojim morate raditi. Čak i ako takva dokumentacija postoji u kodu, ne može se računati na njegovu apsolutnu ispravnost. Ova dokumentacija može biti nejasna, netačna i otvorena za nesporazume. U velikim timovima ili velikim projektima ovaj problem može postati izuzetno akutan.

Dok Python briljira u ranim ili srednjim fazama projekata, u nekom trenutku uspješni projekti i kompanije koje koriste Python mogu se suočiti sa vitalnim pitanjem: „Da li da prepišemo sve na statički otkucanom jeziku?“.

Sistemi za provjeru tipa kao što je mypy rješavaju gornji problem pružajući programeru formalni jezik za opisivanje tipova i provjeravanjem da li deklaracije tipa odgovaraju implementaciji programa (i, opciono, provjerom njihovog postojanja). Generalno, možemo reći da nam ovi sistemi stavljaju na raspolaganje nešto poput pažljivo provjerene dokumentacije.

Upotreba ovakvih sistema ima i druge prednosti, a one su već potpuno netrivijalne:

  • Sistem za provjeru tipa može otkriti neke male (i ne tako male) greške. Tipičan primjer je kada zaborave obraditi vrijednost None ili neki drugi poseban uslov.
  • Refaktoriranje koda je uveliko pojednostavljeno jer je sistem za provjeru tipa često vrlo precizan o tome koji kod treba promijeniti. U isto vrijeme, ne trebamo se nadati 100% pokrivenosti koda testovima, što, u svakom slučaju, obično nije izvodljivo. Ne moramo kopati u dubinu praćenja steka da bismo otkrili uzrok problema.
  • Čak i na velikim projektima, mypy često može obaviti punu provjeru tipa u djeliću sekunde. A izvođenje testova obično traje nekoliko desetina sekundi ili čak minuta. Sistem za provjeru tipa daje programeru trenutnu povratnu informaciju i omogućava mu da brže radi svoj posao. On više ne treba da piše krhke i teške za održavanje jediničnih testova koji zamjenjuju stvarne entitete lažnim i zakrpama samo da bi brže dobio rezultate testiranja koda.

IDE i uređivači kao što su PyCharm ili Visual Studio Code koriste moć napomena tipa da bi programerima omogućili dovršavanje koda, isticanje grešaka i podršku za najčešće korišćene jezičke konstrukcije. A ovo su samo neke od prednosti kucanja. Za neke programere, sve je ovo glavni argument u korist kucanja. To je nešto što koristi odmah nakon implementacije. Ovaj slučaj upotrebe za tipove ne zahtijeva poseban sistem za provjeru tipa kao što je mypy, iako treba napomenuti da mypy pomaže u održavanju napomena tipa u skladu s kodom.

Pozadina mypy

Istorija mypyja počela je u Velikoj Britaniji, u Kembridžu, nekoliko godina pre nego što sam se pridružio Dropbox-u. Bio sam uključen, u okviru svog doktorskog istraživanja, u objedinjavanju statički tipiziranih i dinamičkih jezika. Bio sam inspirisan člankom o inkrementalnom kucanju Jeremyja Sieka i Walida Tahe, te projektom Typed Racket. Pokušao sam da pronađem načine da koristim isti programski jezik za različite projekte - od malih skripti do baza koda koje se sastoje od mnogo miliona linija. Istovremeno, želio sam osigurati da se u projektu bilo kojeg obima ne mora praviti preveliki kompromis. Važan dio svega ovoga bila je ideja o postupnom prelasku sa netipiziranog prototipa projekta na sveobuhvatno testirani statički tipizirani gotov proizvod. Ovih dana se ove ideje uglavnom uzimaju zdravo za gotovo, ali 2010. je to bio problem koji se još uvijek aktivno istraživao.

Moj originalni rad na provjeri tipa nije bio usmjeren na Python. Umjesto toga, koristio sam mali "domaći" jezik Alore. Evo primjera koji će vam omogućiti da shvatite o čemu govorimo (napomene tipa ovdje su opcione):

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

Upotreba pojednostavljenog maternjeg jezika uobičajen je pristup koji se koristi u naučnim istraživanjima. To je tako, ne samo zato što vam omogućava brzo izvođenje eksperimenata, a i zbog činjenice da se ono što nema nikakve veze sa studijom može lako zanemariti. Programski jezici u stvarnom svijetu obično su fenomeni velikih razmjera sa složenim implementacijama, a to usporava eksperimentiranje. Međutim, bilo kakvi rezultati zasnovani na pojednostavljenom jeziku izgledaju pomalo sumnjivo, jer je u dobijanju ovih rezultata istraživač možda žrtvovao razmatranja važna za praktičnu upotrebu jezika.

Moj program za proveru tipa za Alore je izgledao veoma obećavajuće, ali sam želeo da ga testiram eksperimentišući sa pravim kodom, koji, možete reći, nije napisan u Aloreu. Na moju sreću, Alore jezik je uglavnom bio zasnovan na istim idejama kao i Python. Bilo je dovoljno lako promijeniti tip za provjeru kako bi mogao raditi sa Pythonovom sintaksom i semantikom. To nam je omogućilo da pokušamo provjeriti tip u open source Python kodu. Osim toga, napisao sam transpiler za pretvaranje koda napisanog u Alore u Python kod i koristio ga za prevođenje koda za proveru tipova. Sada sam imao sistem za proveru tipa napisan u Python-u koji podržava podskup Pythona, neku vrstu tog jezika! (Određene arhitektonske odluke koje su imale smisla za Alore bile su loše prikladne za Python, a to je još uvijek primjetno u dijelovima mypy kodne baze.)

U stvari, jezik koji podržava moj sistem tipova u ovom trenutku se ne bi mogao sasvim nazvati Python: to je bila varijanta Pythona zbog nekih ograničenja sintakse napomena tipa Python 3.

Izgledalo je kao mješavina Jave i Pythona:

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

Jedna od mojih ideja u to vrijeme bila je da koristim napomene tipa za poboljšanje performansi kompajliranjem ove vrste Pythona u C, ili možda JVM bajtkod. Došao sam do faze pisanja prototipa kompajlera, ali sam napustio ovu ideju, pošto je sama provera tipa izgledala prilično korisno.

Na kraju sam predstavio svoj projekat na PyCon 2013 u Santa Clari. Razgovarao sam o tome i sa Gvidom van Rosumom, doživotnim dobroćudnim Python diktatorom. Ubedio me je da odbacim svoju sintaksu i da se držim standardne sintakse Python 3. Python 3 podržava napomene funkcija, tako da bi moj primjer mogao biti prepisan kao što je prikazano ispod, što rezultira normalnim Python programom:

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

Morao sam napraviti neke kompromise (prije svega, želim napomenuti da sam izmislio vlastitu sintaksu upravo iz tog razloga). Konkretno, Python 3.3, najnovija verzija jezika u to vrijeme, nije podržavao promjenjive napomene. Razgovarao sam s Guidom putem e-pošte o raznim mogućnostima sintaktičkog dizajna takvih napomena. Odlučili smo koristiti komentare tipa za varijable. Ovo je služilo predviđenoj svrsi, ali je bilo pomalo glomazno (Python 3.6 nam je dao ljepšu sintaksu):

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

Komentari tipa također su bili korisni za podršku Python 2, koji nema ugrađenu podršku za napomene tipa:

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

Ispostavilo se da ovi (i drugi) kompromisi zapravo nisu bitni - prednosti statičkog kucanja značile su da su korisnici ubrzo zaboravili na manje od idealne sintakse. Budući da u Python kodu sa provjerom tipa nisu korištene posebne sintaktičke konstrukcije, postojeći Python alati i procesi obrade koda nastavili su normalno raditi, što je programerima znatno olakšalo učenje novog alata.

Guido me je također uvjerio da se pridružim Dropboxu nakon što sam završio diplomski rad. Ovdje počinje najzanimljiviji dio mypy priče.

Da se nastavi ...

Dragi čitaoci! Ako koristite Python, recite nam o obimu projekata koje razvijate na ovom jeziku.

Put do provjere tipa 4 miliona linija Python koda. Dio 1
Put do provjere tipa 4 miliona linija Python koda. Dio 1

izvor: www.habr.com

Dodajte komentar