Cesta ke kontrole typu 4 milionů řádků kódu Pythonu. Část 1

Dnes vám dáváme do pozornosti první část překladu materiálu o tom, jak se Dropbox zabývá typovou kontrolou kódu Python.

Cesta ke kontrole typu 4 milionů řádků kódu Pythonu. Část 1

Dropbox hodně píše v Pythonu. Je to jazyk, který používáme extrémně široce, jak pro back-endové služby, tak pro desktopové klientské aplikace. Hodně také používáme Go, TypeScript a Rust, ale Python je náš hlavní jazyk. Vzhledem k našemu měřítku, a to mluvíme o milionech řádků kódu Pythonu, se ukázalo, že dynamické psaní takového kódu zbytečně zkomplikovalo jeho pochopení a začalo vážně ovlivňovat produktivitu práce. Abychom tento problém zmírnili, začali jsme postupně přecházet náš kód na statickou kontrolu typu pomocí mypy. Toto je pravděpodobně nejoblíbenější samostatný systém kontroly typů pro Python. Mypy je open source projekt, jeho hlavní vývojáři pracují v Dropboxu.

Dropbox byl jednou z prvních společností, které implementovaly statickou kontrolu typu v kódu Pythonu v tomto měřítku. Mypy se v dnešní době používá v tisících projektů. Tento nástroj nesčetněkrát, jak se říká, „testován v bitvě“. Ušli jsme dlouhou cestu, abychom se dostali tam, kde jsme nyní. Na cestě bylo mnoho neúspěšných podniků a neúspěšných experimentů. Tento příspěvek pokrývá historii statické kontroly typu v Pythonu, od jejích skalnatých začátků jako součásti mého výzkumného projektu až po současnost, kdy se kontrola typu a napovídání typu staly samozřejmostí pro nespočet vývojářů, kteří píší v Pythonu. Tyto mechanismy jsou nyní podporovány mnoha nástroji, jako jsou IDE a analyzátory kódu.

Přečtěte si druhou část

Proč je nutná kontrola typu?

Pokud jste někdy používali dynamicky typovaný Python, možná máte nějaké nejasnosti, proč je kolem statického psaní a mypy v poslední době takový povyk. Nebo možná máte rádi Python právě kvůli jeho dynamickému psaní a to, co se děje, vás prostě rozčiluje. Klíčem k hodnotě statického psaní je škála řešení: čím větší je váš projekt, tím více se přikláníte ke statickému psaní, a nakonec, tím více ho skutečně potřebujete.

Předpokládejme, že určitý projekt dosáhl velikosti desítek tisíc řádků a ukázalo se, že na něm pracuje několik programátorů. Při pohledu na podobný projekt můžeme na základě našich zkušeností říci, že pochopení jeho kódu bude klíčem k udržení produktivity vývojářů. Bez typových anotací může být obtížné například zjistit, jaké argumenty předat funkci nebo jaké typy funkce může vrátit. Zde jsou typické otázky, na které je často obtížné odpovědět bez použití typových poznámek:

  • Může se tato funkce vrátit None?
  • Jaký by měl být tento argument? items?
  • Jaký je typ atributu id: int je to, str, nebo možná nějaký vlastní typ?
  • Měl by být tento argument seznamem? Je možné mu předat n-tici?

Pokud se podíváte na následující typově anotovaný úryvek kódu a pokusíte se odpovědět na podobné otázky, ukáže se, že jde o nejjednodušší úkol:

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

  • read_metadata nevrací None, protože návratový typ není Optional[…].
  • argument items je posloupnost řádků. Nelze jej opakovat náhodně.
  • Atribut id je řetězec bajtů.

V ideálním světě by se dalo očekávat, že všechny takové jemnosti budou popsány ve vestavěné dokumentaci (docstring). Zkušenost ale dává mnoho příkladů toho, že taková dokumentace není v kódu, se kterým musíte pracovat, často dodržována. I když je taková dokumentace v kódu přítomna, nelze počítat s její absolutní správností. Tato dokumentace může být vágní, nepřesná a může vést k nedorozuměním. Ve velkých týmech nebo velkých projektech se tento problém může stát extrémně akutním.

Zatímco Python vyniká v raných nebo středních fázích projektů, v určitém okamžiku mohou úspěšné projekty a společnosti, které Python používají, čelit zásadní otázce: „Máme vše přepsat do staticky typovaného jazyka?“.

Systémy kontroly typů, jako je mypy, řeší výše uvedený problém tím, že vývojáři poskytnou formální jazyk pro popis typů a zkontrolují, zda deklarace typu odpovídají implementaci programu (a volitelně i kontrolou jejich existence). Obecně lze říci, že tyto systémy nám dávají k dispozici něco jako pečlivě zkontrolovanou dokumentaci.

Použití takových systémů má další výhody a jsou již zcela netriviální:

  • Systém kontroly typu dokáže detekovat některé malé (a ne tak malé) chyby. Typickým příkladem je, když zapomenou zpracovat hodnotu None nebo nějakou jinou zvláštní podmínku.
  • Refaktorování kódu je značně zjednodušeno, protože systém kontroly typu je často velmi přesný v tom, jaký kód je třeba změnit. Zároveň nemusíme doufat ve 100% pokrytí kódem pomocí testů, což v žádném případě většinou není realizovatelné. Abychom zjistili příčinu problému, nemusíme se ponořit do hloubek trasování zásobníku.
  • Dokonce i na velkých projektech může mypy často provést úplnou kontrolu typu ve zlomku sekundy. A provedení testů obvykle trvá desítky sekund nebo dokonce minuty. Systém kontroly typu poskytuje programátorovi okamžitou zpětnou vazbu a umožňuje mu dělat jeho práci rychleji. Už nepotřebuje psát křehké a obtížně udržovatelné jednotkové testy, které nahrazují skutečné entity maketami a záplatami, jen aby rychleji získal výsledky testů kódu.

IDE a editory, jako je PyCharm nebo Visual Studio Code, využívají sílu typových anotací k tomu, aby vývojářům poskytovaly dokončování kódu, zvýraznění chyb a podporu běžně používaných jazykových konstrukcí. A to jsou jen některé z výhod psaní. Pro některé programátory je toto vše hlavním argumentem ve prospěch psaní. To je něco, co je přínosné hned po implementaci. Tento případ použití pro typy nevyžaduje samostatný systém kontroly typu jako mypy, i když je třeba poznamenat, že mypy pomáhá udržovat anotace typu konzistentní s kódem.

Pozadí mypy

Historie mypy začala ve Velké Británii, v Cambridge, několik let předtím, než jsem nastoupil do Dropboxu. V rámci svého doktorského výzkumu jsem se podílel na sjednocení staticky typovaných a dynamických jazyků. Inspiroval mě článek o inkrementálním psaní od Jeremyho Sieka a Walida Taha a projekt Typed Racket. Snažil jsem se najít způsoby, jak používat stejný programovací jazyk pro různé projekty – od malých skriptů až po základny kódu sestávající z mnoha milionů řádků. Zároveň jsem chtěl zajistit, aby v projektu jakéhokoli rozsahu člověk nemusel dělat příliš velké kompromisy. Důležitou součástí toho všeho byla myšlenka postupného přechodu od projektu netypizovaného prototypu ke komplexně testovanému staticky typovanému hotovému produktu. V dnešní době jsou tyto myšlenky z velké části považovány za samozřejmé, ale v roce 2010 to byl problém, který se stále aktivně zkoumal.

Moje původní práce v kontrole typu nebyla zaměřena na Python. Místo toho jsem použil malý „domácí“ jazyk Alore. Zde je příklad, který vám umožní pochopit, o čem mluvíme (anotace typu jsou zde volitelné):

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

Používání zjednodušeného mateřského jazyka je běžný přístup používaný ve vědeckém výzkumu. Je tomu tak v neposlední řadě proto, že umožňuje rychle provádět experimenty, a také proto, že to, co nemá se studií nic společného, ​​lze snadno ignorovat. Programovací jazyky v reálném světě mají tendenci být rozsáhlými jevy se složitými implementacemi, což zpomaluje experimentování. Jakékoli výsledky založené na zjednodušeném jazyce však vypadají trochu podezřele, protože při získávání těchto výsledků výzkumník možná obětoval úvahy důležité pro praktické použití jazyků.

Moje kontrola typu pro Alore vypadala velmi slibně, ale chtěl jsem ji vyzkoušet experimentováním se skutečným kódem, který, jak byste řekli, nebyl napsán v Alore. Naštěstí pro mě byl jazyk Alore z velké části založen na stejných myšlenkách jako Python. Bylo dost snadné předělat typecker tak, aby fungoval se syntaxí a sémantikou Pythonu. To nám umožnilo vyzkoušet kontrolu typu v open source kódu Pythonu. Kromě toho jsem napsal transpiler pro převod kódu napsaného v Alore do kódu Python a použil jsem jej k překladu kódu typu typecker. Nyní jsem měl systém kontroly typu napsaný v Pythonu, který podporoval podmnožinu Pythonu, nějaký druh tohoto jazyka! (Některá architektonická rozhodnutí, která dávala smysl pro Alore, se pro Python nehodila, a to je stále patrné v částech kódové základny mypy.)

Ve skutečnosti jazyk podporovaný mým typovým systémem nemohl být v tuto chvíli zcela nazván Python: byla to varianta Pythonu kvůli určitým omezením syntaxe anotací typu Python 3.

Vypadalo to jako směs Javy a Pythonu:

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

Jedním z mých tehdejších nápadů bylo použít typové anotace ke zlepšení výkonu kompilací tohoto druhu Pythonu do C, nebo možná JVM bytecode. Dostal jsem se do fáze psaní prototypu kompilátoru, ale tento nápad jsem opustil, protože samotná kontrola typu vypadala docela užitečně.

Svůj projekt jsem nakonec prezentoval na PyCon 2013 v Santa Claře. Mluvil jsem o tom také s Guido van Rossumem, benevolentním pythonským diktátorem na celý život. Přesvědčil mě, abych upustil od své vlastní syntaxe a zůstal u standardní syntaxe Pythonu 3. Python 3 podporuje anotace funkcí, takže můj příklad by mohl být přepsán, jak je ukázáno níže, což vede k normálnímu programu Python:

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

Musel jsem udělat nějaké kompromisy (především chci poznamenat, že jsem si vymyslel vlastní syntaxi právě z tohoto důvodu). Zejména Python 3.3, nejnovější verze jazyka v té době, nepodporoval proměnné anotace. S Guidem jsem e-mailem probral různé možnosti syntaktického návrhu takových anotací. Rozhodli jsme se použít komentáře typu pro proměnné. To posloužilo zamýšlenému účelu, ale bylo to poněkud těžkopádné (Python 3.6 nám dal hezčí syntaxi):

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

Typové komentáře se také hodily pro podporu Pythonu 2, který nemá vestavěnou podporu pro typové anotace:

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

Ukázalo se, že na těchto (a dalších) kompromisech vlastně nezáleží – výhody statického psaní znamenaly, že uživatelé brzy zapomněli na nepříliš dokonalou syntaxi. Vzhledem k tomu, že v kódu Pythonu s kontrolou typu nebyly použity žádné speciální syntaktické konstrukce, stávající nástroje Pythonu a procesy zpracování kódu nadále fungovaly normálně, což vývojářům výrazně usnadnilo naučit se nový nástroj.

Guido mě také přesvědčil, abych se připojil k Dropboxu poté, co jsem dokončil svou diplomovou práci. Zde začíná nejzajímavější část příběhu mypy.

Chcete-li se pokračovat ...

Vážení čtenáři! Pokud používáte Python, řekněte nám prosím o rozsahu projektů, které v tomto jazyce vyvíjíte.

Cesta ke kontrole typu 4 milionů řádků kódu Pythonu. Část 1
Cesta ke kontrole typu 4 milionů řádků kódu Pythonu. Část 1

Zdroj: www.habr.com

Přidat komentář