Cesta k typovej kontrole 4 miliónov riadkov kódu Python. Časť 1

Dnes vám dávame do pozornosti prvú časť prekladu materiálu o tom, ako sa Dropbox vysporiadava s ovládaním typu kódu Python.

Cesta k typovej kontrole 4 miliónov riadkov kódu Python. Časť 1

Dropbox veľa píše v Pythone. Toto je jazyk, ktorý používame extrémne široko – ako pre backendové služby, tak aj pre desktopové klientske aplikácie. Veľa používame aj Go, TypeScript a Rust, ale Python je náš hlavný jazyk. Vzhľadom na náš rozsah, a to hovoríme o miliónoch riadkov kódu Python, sa ukázalo, že dynamické písanie takéhoto kódu zbytočne skomplikovalo jeho pochopenie a začalo vážne ovplyvňovať produktivitu práce. Na zmiernenie tohto problému sme začali postupne prechádzať náš kód na statickú kontrolu typu pomocou mypy. Toto je pravdepodobne najpopulárnejší samostatný systém kontroly typov pre Python. Mypy je open source projekt, jeho hlavní vývojári pracujú v Dropboxe.

Dropbox bol jednou z prvých spoločností, ktoré implementovali statickú kontrolu typu v kóde Python v tomto rozsahu. Mypy sa v súčasnosti používa v tisíckach projektov. Tento nástroj bol, ako sa hovorí, nespočetnekrát „testovaný v boji“. Museli sme prejsť dlhú cestu, aby sme sa dostali tam, kde sme teraz. Na ceste bolo veľa neúspešných snáh a neúspešných experimentov. Tento článok zaznamenáva históriu statickej kontroly typov v Pythone, od jej skalných začiatkov ako súčasti môjho akademického výskumného projektu až po dnešok, keď sa typové kontroly a tipy na typy stali bežnými medzi nespočetnými vývojármi Pythonu. Tieto mechanizmy sú teraz podporované rôznymi nástrojmi, ako sú IDE a analyzátory kódu.

Prečítajte si druhú časť

Prečo je potrebná kontrola typu?

Ak ste niekedy používali dynamicky písaný Python, možno ste trochu zmätení, prečo je v poslednej dobe toľko rozruchu okolo statického písania a mypy. Alebo sa vám môže stať, že máte radi Python práve pre jeho dynamické písanie a to, čo sa deje, vás jednoducho rozčuľuje. Kľúčom k hodnote statického písania je rozsah rozhodnutí: čím väčší je váš projekt, tým viac sa prikláňate k statickému písaniu a v konečnom dôsledku ho skutočne potrebujete.

Povedzme, že projekt dosiahol veľkosť desiatok tisíc riadkov a ukázalo sa, že na ňom pracuje niekoľko programátorov. Keď uvažujeme o takomto projekte, na základe našich skúseností môžeme povedať, že pochopenie jeho kódu bude kľúčové pre udržanie produktivity vývojárov. Bez anotácií typu môže byť ťažké napríklad zistiť, aké argumenty by sa mali funkcii odovzdať alebo aké typy hodnôt môže určitá funkcia vrátiť. Tu sú typické otázky, na ktoré je často ťažké odpovedať bez použitia anotácií typu:

  • Môže sa táto funkcia vrátiť None?
  • Aký by mal byť tento argument? items?
  • Aký je typ atribútu id: int je to, str, alebo možno nejaký vlastný typ?
  • Mal by byť tento argument zoznamom? Dá sa do nej prejsť n-tica?

Ak sa pozriete na nasledujúci útržok kódu s anotáciou a pokúsite sa odpovedať na podobné otázky, ukáže sa, že ide o jednoduchú úlohu:

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

  • read_metadata nevracia Nonepretože návratový typ nie je Optional[…].
  • argument items je postupnosť riadkov. Nedá sa opakovať v akomkoľvek poradí.
  • Atribút id je reťazec bajtov.

V ideálnom svete by sa dalo očakávať, že všetky takéto jemnosti budú popísané v zabudovanej dokumentácii (docstring). Skúsenosti však poskytujú veľa príkladov, že takáto dokumentácia sa často nedodržiava v kóde, s ktorým musíte pracovať. Aj keď je takáto dokumentácia v kóde prítomná, nemôžete počítať s jej absolútnou správnosťou. Táto dokumentácia môže byť nejasná, nepresná a môže viesť k nedorozumeniu. Vo veľkých tímoch alebo veľkých projektoch sa tento problém môže stať mimoriadne akútnym.

Zatiaľ čo Python vyniká v projektoch na začiatku alebo v strednej fáze, v určitom bode môžu úspešné projekty a spoločnosti, ktoré používajú Python, čeliť dôležitej otázke: „Mali by sme všetko prepísať do staticky napísaného jazyka?“

Systémy na kontrolu typov, ako je mypy, riešia vyššie uvedený problém tým, že vývojárovi poskytnú formálny jazyk na popis typov a skontrolujú, či deklarácie typu zodpovedajú implementácii programu (a voliteľne skontrolujú ich existenciu). Vo všeobecnosti sa dá povedať, že tieto systémy nám poskytujú niečo ako starostlivo overenú dokumentáciu.

Použitie takýchto systémov má ďalšie výhody a sú úplne netriviálne:

  • Systém kontroly typu dokáže odhaliť niektoré menšie (a tiež nie také menšie) chyby. Typickým príkladom je, keď zabudnú spracovať hodnotu None alebo nejaká iná špeciálna podmienka.
  • Refaktorovanie kódu je značne zjednodušené, pretože kontrola typu vám často presne povie, aký kód je potrebné zmeniť. Zároveň sa nemusíme spoliehať na 100% testovacie pokrytie kódu, čo je väčšinou v žiadnom prípade nemožné. Aby sme zistili, čo je zlé, nemusíme sa hrabať hlboko v správach sledovania zásobníka.
  • Dokonca aj na veľkých projektoch môže mypy často vykonať úplnú kontrolu typu v zlomku sekundy. A spustenie testov zvyčajne trvá desiatky sekúnd alebo dokonca minúty. Systém typovej kontroly dáva programátorovi okamžitú spätnú väzbu a umožňuje mu robiť svoju prácu rýchlejšie. Už nemusí písať krehké a ťažko udržiavateľné testy jednotiek, ktoré nahrádzajú skutočné entity falošnými a záplatami, len aby získal rýchlejšie výsledky testovania kódu.

IDE a editory ako PyCharm alebo Visual Studio Code využívajú silu typových anotácií, aby vývojárom poskytli automatické dokončovanie kódu, zvýrazňovanie chýb a podporu pre bežne používané jazykové konštrukcie. A to sú len niektoré z výhod, ktoré typizácia poskytuje. Pre niektorých programátorov je toto všetko hlavným argumentom v prospech písania. To je niečo, čo prináša výhody hneď po implementácii. Tento prípad použitia pre typy nevyžaduje samostatný systém kontroly typu, ako je napríklad mypy, aj keď treba poznamenať, že mypy pomáha udržiavať konzistenciu medzi anotáciami typu a kódom.

Mypy pozadie

Mypy príbeh sa začal vo Veľkej Británii, v Cambridge, niekoľko rokov predtým, ako som sa pripojil k Dropboxu. V rámci doktorandského výskumu som sa venoval problematike zjednocovania staticky typizovaných a dynamických jazykov. Inšpiroval ma článok o inkrementálnom písaní od Jeremyho Sika a Walida Taha, ako aj projekt Typed Racket. Snažil som sa nájsť spôsoby, ako použiť rovnaký programovací jazyk pre rôzne projekty – od malých skriptov až po kódové bázy pozostávajúce z mnohých miliónov riadkov. Zároveň som chcel zabezpečiť, aby som pri projekte akéhokoľvek rozsahu nemusel robiť príliš veľa kompromisov. Dôležitou súčasťou toho všetkého bola myšlienka postupného prechodu od projektu prototypu bez typu ku komplexne testovanému, staticky typizovanému hotovému produktu. Tieto myšlienky sú v dnešnej dobe do značnej miery samozrejmosťou, no v roku 2010 to bola otázka, ktorá sa stále aktívne skúmala.

Moja počiatočná práca pri kontrole typu nebola zameraná na Python. Namiesto toho som použil malý „domáci“ jazyk Alore. Tu je príklad, ktorý vám poskytne predstavu o tom, o čom hovoríme (anotácie typu sú voliteľné):

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

Používanie zjednodušeného jazyka vlastného dizajnu je bežný prístup používaný vo vedeckom výskume. Je to tak v neposlednom rade preto, že to umožňuje rýchle vykonávanie experimentov a tiež preto, že to, čo nie je relevantné pre výskum, možno ľahko ignorovať. Programovacie jazyky v reálnom živote majú tendenciu byť rozsiahlymi javmi so zložitými implementáciami, čo spomaľuje experimentovanie. Akékoľvek výsledky založené na zjednodušenom jazyku sú však trochu podozrivé, pretože pri získavaní týchto výsledkov výskumník možno obetoval úvahy dôležité pre praktické používanie jazykov.

Môj typ kontroly pre Alore vyzeral veľmi sľubne, ale chcel som ho otestovať experimentovaním so skutočným kódom, ktorý v skutočnosti nebol napísaný v Alore. Našťastie pre mňa bol jazyk Alore z veľkej časti založený na rovnakých myšlienkach ako Python. Bolo dosť jednoduché prepracovať typ kontroly tak, aby fungoval so syntaxou a sémantikou Pythonu. To nám umožnilo vyskúšať vykonanie kontroly typu v open source kóde Pythonu. Napísal som tiež transpiler na konverziu kódu Alore na kód Python a použil som ho na preklad kódu kontroly typu. Teraz som mal systém kontroly typu napísaný v Pythone, ktorý podporoval podmnožinu Pythonu, nejakú variáciu tohto jazyka! (Určité architektonické rozhodnutia, ktoré dávali zmysel pre Alore, neboli pre Python vhodné; to je stále evidentné v niektorých častiach kódovej základne mypy.)

V skutočnosti sa jazyk podporovaný mojím typovým systémom nedal v tomto bode celkom nazvať Python: bol to variant Pythonu kvôli určitým obmedzeniam syntaxe anotácií typu Python 3.

Vyzeralo to ako zmes Javy a Pythonu:

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

Jedným z mojich nápadov v tom čase bolo použiť anotácie typu na zlepšenie výkonu kompilovaním tejto verzie Pythonu do C alebo možno do bajtkódu JVM. Dostal som sa do fázy písania prototypového kompilátora, ale opustil som túto myšlienku, pretože samotná kontrola typu vyzerala celkom užitočne.

Nakoniec som svoj projekt prezentoval na PyCon 2013 v Santa Clare. Hovoril som o tom aj s Guidom van Rossumom, doživotným benevolentným diktátorom Pythonu. Presvedčil ma, aby som sa vzdal svojej vlastnej syntaxe a zostal pri štandardnej syntaxi Pythonu 3. Python 3 podporuje anotácie funkcií, takže môj príklad by sa dal prepísať ako je uvedené nižšie, výsledkom čoho je normálny program Python:

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

Musel som urobiť nejaké kompromisy (v prvom rade chcem upozorniť, že preto som si vymyslel vlastnú syntax). Najmä Python 3.3, najnovšia verzia jazyka v tom čase, nepodporovala premenlivé anotácie. Diskutoval som o rôznych syntaktických možnostiach takýchto anotácií s Guidom prostredníctvom e-mailu. Rozhodli sme sa použiť komentáre typu pre premenné. Tým sa dosiahol cieľ, ale vyzeralo to trochu ťažkopádne (Python 3.6 nám dal krajšiu syntax):

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

Komentáre typu sú tiež užitočné na podporu Pythonu 2, ktorý nemá vstavanú podporu pre anotácie typu:

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

Ukázalo sa, že na týchto (a ďalších) kompromisoch vlastne nezáleží – výhody statického písania spôsobili, že používatelia čoskoro zabudli na nie dokonalú syntax. Keďže kód Pythonu, ktorý ovládal typy, už nepoužíval špeciálnu syntax, existujúce nástroje Pythonu a procesy kódu naďalej fungovali normálne, čo vývojárom uľahčilo naučiť sa nový nástroj.

Guido ma tiež presvedčil, aby som sa pripojil k Dropboxu po dokončení mojej záverečnej práce. Tu sa začína najzaujímavejšia vec v histórii mypy.

Ak sa chcete pokračovať ...

Vážení čitatelia! Ak používate Python, povedzte nám o rozsahu projektov, ktoré v tomto jazyku vyvíjate.

Cesta k typovej kontrole 4 miliónov riadkov kódu Python. Časť 1
Cesta k typovej kontrole 4 miliónov riadkov kódu Python. Časť 1

Zdroj: hab.com

Pridať komentár