Cesta k ověřování typů 4 milionů řádků kódu v Pythonu. Část 1

Dnes vám nabízíme první část překladu materiálu o tom, jak v Dropboxu probíhá kontrola typů Python kódu.

Cesta k ověřování typů 4 milionů řádků kódu v Pythonu. Část 1

V Dropboxu se hodně píše v Pythonu. Tento jazyk používáme velmi široce - jak pro backendové služby, tak pro desktopové klientské aplikace. Také ve velkém měřítku aplikujeme Go, TypeScript a Rust, ale Python je naším hlavním jazykem. Vzhledem k našim rozměrům, mluvíme o milionech řádcích Python kódu, se ukázalo, že dynamická typizace takového kódu nepřiměřeně ztěžuje jeho pochopení a začíná vážně ovlivňovat produktivitu práce. Abychom tuto problém zmírnili, začali jsme postupný přechod našeho kódu na statickou kontrolu typů pomocí mypy. To je pravděpodobně nejpopulárnější samostatný systém kontroly typů pro Python. Mypy je projekt s otevřeným zdrojovým kódem, jeho hlavní vývojáři pracují v Dropboxu.

Dropbox se stal jednou z prvních společností, která zavedla statickou kontrolu typů v Python kódu v takovém měřítku. Dnes je mypy používáno v tisících projektů. Tento nástroj byl nespočetkrát, jak se říká, „prověřen v boji“. Abychom se dostali tam, kde jsme nyní, museli jsme projít dlouhou cestu. Na této cestě bylo mnoho neúspěšných pokusů a nezdarů. Tento materiál vypráví příběh statické kontroly typů v Pythonu - od jejího složitého začátku, který byl součástí mého vědeckého výzkumného projektu, až po dnešek, kdy se kontroly typů a návrhy typů staly běžnými pro nespočet vývojářů, kteří píší v Pythonu. Tyto mechanizmy nyní podporuje řada nástrojů - jako jsou IDE a analyzátory kódu.

→ Číst druhou část

Proč je kontrola typů potřebná?

Pokud jste někdy používali dynamicky typovaný Python, může vám být obtížné pochopit, proč se kolem statické typizace a mypy v poslední době zvedla taková pozornost. Možná se vám právě dynamická typizace Pythonu líbí, a to, co se děje, vás prostě rozčiluje. Klíčem k hodnotě statické typizace je rozsah řešení: čím větší je váš projekt, tím více se kloníte k statické typizaci, a nakonec tím více ji opravdu potřebujete.

Předpokládejme, že určitý projekt dosáhl velikosti v desítkách tisíc řádků a ukázalo se, že na něm pracuje několik programátorů. Při zvažování takového projektu můžeme vycházet ze svých zkušeností a říci, že porozumění jeho kódu se stane klíčem k udržení produktivity vývojářů. Bez anotací typů bývá obtížné zjistit například to, jaké argumenty je třeba předat funkci, nebo jaké typy hodnot může určitá funkce vracet. Zde jsou typické otázky, na které často není snadné odpovědět bez použití anotací typů:

  • Může tato funkce vrátit None?
  • Čím by měl být tento argument items?
  • Jaký je typ atributu id: int je to str, nebo snad nějaký uživatelský typ?
  • Musí být tento argument seznamem? Může mu být předán n-tice?

Pokud se podíváme na následující kódový úryvek s anotacemi typů a pokusíme se odpovědět na podobné otázky, zjistíme, že je to velmi jednoduchý úkol:

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

  • read_metadata nevrací None, protože navrácený typ není Optional[…].
  • Argument items — to je sekvence řetězců. Není možné ji iterovat v libovolném pořadí.
  • Atribut id — to je řetězec bajtů.

V ideálním světě bychom mohli očekávat, že všechny takové nuance budou popsány ve vestavěné dokumentaci (docstring). Ale zkušenosti nabízejí řadu příkladů toho, že taková dokumentace v kódu, se kterým musíme pracovat, často chybí. I když taková dokumentace v kódu existuje, nelze se spoléhat na její absolutní správnost. Tato dokumentace může být nejasná, nepřesná a může zanechávat řadu možností pro její nesprávné pochopení. V velkých týmech nebo ve velkých projektech se tento problém může stát mimořádně vážným.

Ačkoli se Python výborně osvědčuje v raných nebo středních fázích projektů, v určitém okamžiku se úspěšné projekty a společnosti, které používají Python, mohou setkat s životně důležitou otázkou: „Musíme přepsat vše na staticky typizovaný jazyk?“

Systémy kontroly typů, jako je mypy, řeší výše uvedený problém tím, že vývojářům poskytují formální jazyk pro popis typů a tím, že kontrolují, zda popisy typů odpovídají implementaci programů (a, co není nezbytné, kontrolují jejich existenci). Obecně lze říci, že tyto systémy nám poskytují něco jako pečlivě ověřenou dokumentaci.

Použití takových systémů má i další výhody, které jsou zcela nesmírně důležité:

  • Systém kontroly typů může odhalit některé drobné (ale i nesmírně významné) chyby. Typickým příkladem je, když zapomeneme zpracovat hodnotu None nebo nějakou jinou specifickou podmínku.
  • Značně se zjednodušuje refaktorování kódu, protože systém kontroly typů často velmi přesně udává, jaký kód je třeba změnit. A přitom se nemusíme spoléhat na 100% pokrytí kódu testy, což je v každém případě obvykle neproveditelné. Nemusíme prozkoumávat hloubky výpisů zásobníku, abychom zjistili příčinu závady.
  • I v rozsáhlých projektech může mypy často provést úplnou kontrolu typů během zlomku sekundy. A provádění testů obvykle trvá desítky sekund nebo dokonce minuty. Systém kontroly typů poskytuje programátorovi okamžitou zpětnou vazbu a umožňuje mu rychleji vykonávat svou práci. Není nutné psát křehké a těžko udržovatelné modulové testy, které nahrazují skutečné entity mocky a patche jen proto, aby se rychleji dostaly výsledky testování kódu.

IDE a editory, jako PyCharm nebo Visual Studio Code, využívají možnosti anotací typů k poskytování vývojářům funkcí automatického dokončování kódu, zvýraznění chyb a podpory často používaných jazykových konstrukcí. A to je pouze několik z výhod, které typizace přináší. Pro některé programátory je to hlavní argument pro typizaci. To, co přináší prospěch ihned po zavedení do práce. Tento způsob využití typů nevyžaduje aplikaci samostatného systému kontroly typů, jako je mypy, i když je třeba poznamenat, že mypy pomáhá udržovat shodu mezi anotacemi typů a kódem.

Historie mypy

Historie mypy začala ve Velké Británii, v Cambridge, několik let předtím, než jsem se připojil k Dropboxu. Věnoval jsem se v rámci doktorského výzkumu otázce unifikace staticky typovaných a dynamických jazyků. Inspirovala mě článek o postupné typizaci od Jeremyho Sieka a Valida Taha, stejně jako projekt Typed Racket. Snažil jsem se najít způsoby, jak použít stejný programovací jazyk pro různé projekty - od malých skriptů po kódové základny skládající se z mnoha milionů řádků. Chtěl jsem, aby v projektu jakékoli velikosti nebylo nutné jít na příliš velké kompromisy. Důležitou součástí toho všeho byla myšlenka postupného přechodu od netypizovaného prototypu projektu k všestranně testovanému staticky typovanému hotovému produktu. Tyto myšlenky jsou dnes do značné míry považovány za samozřejmost, ale v roce 2010 to byl problém, který se stále aktivně zkoumal.

Moje počáteční práce v oblasti kontroly typů nebyla zaměřena na Python. Místo toho jsem použil malý "vlastní" jazyk AloreToto je příklad, který vám pomůže pochopit, o čem je řeč (typové anotace 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 jazyka vlastní konstrukce je běžný přístup v akademickém výzkumu. Důvodem je mimo jiné to, že to umožňuje rychle provádět experimenty a že to, co není relevantní k výzkumu, lze bez problémů ignorovat. Skutečné programovací jazyky obvykle představují rozsáhlé jevy se složitými implementacemi, což experimenty zpomaluje. Ale jakékoli výsledky založené na zjednodušeném jazyce vypadají poněkud podezřele, protože při získávání těchto výsledků mohl výzkumník obětovat faktory důležité pro praktické používání jazyků.

Můj typový kontrolor pro Alore vypadal velmi nadějně, ale chtěl jsem jej otestovat experimenty se skutečným kódem, který, jak se dá říct, nebyl na Alore napsán. Naštěstí byl jazyk Alore v podstatě založen na stejných myšlenkách jako Python. Bylo dost jednoduché přepracovat typový kontrolor tak, aby mohl pracovat se syntaxí a sémantikou Pythonu. To umožnilo vyzkoušet typovou kontrolu na open-source Python kódu. Kromě toho jsem napsal transpiler pro převod kódu napsaného v Alore do Python kódu a použil ho k překladu kódu mého typového kontroloru. Nyní jsem měl systém pro typovou kontrolu napsaný v Pythonu, který podporoval podmnožinu Pythonu, jakýsi druh tohoto jazyka! (Některá architektonická rozhodnutí, která dávala smysl pro Alore, byla pro Python nevhodná, což je stále patrné v některých částech kódové základny mypy.)

Ve skutečnosti jazyk podporovaný mým typovým systémem touto chvílí nebylo možné úplně nazvat Pythonem: byl to variant Pythonu kvůli některým syntaktickým omezením typových anotací Pythonu 3.

Vypadalo to jako směs Java a Pythonu:

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

Jedním z mých nápadů tehdy bylo využít typové anotace ke zlepšení výkonu tím, že tuto variantu Pythonu zkompiluji do C, nebo možná do bajtkódu JVM. Pokročil jsem do fáze psaní prototypu kompilátoru, ale tuto myšlenku jsem opustil, protože samotná kontrola typů se zdála být dost užitečná.

Nakonec jsem svůj projekt představil na konferenci PyCon 2013 v Santa Clara. Také jsem o tom mluvil s Guidem van Rossumem, laskavým doživotním diktátorem Pythonu. Přesvědčil mě, abych se vzdal vlastního syntaktického stylu a držel se standardního syntaktického stylu Pythonu 3. Python 3 podporuje anotace funkcí, a proto bylo možné můj příklad přepsat tak, jak je uvedeno níže, a získat běžný Python program:

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

Musel jsem učinit určité kompromisy (v první řadě chci zdůraznit, že jsem vymýšlel svůj vlastní syntaktický styl právě proto). Konkrétně Python 3.3, tehdy nejnovější verze jazyka, nepodporoval anotace proměnných. S Guidem jsme e-mailem diskutovali o různých možnostech syntaktického formátování takovýchto anotací. Rozhodli jsme se použít pro proměnné komentáře s typy. To nám umožnilo dosáhnout cíle, ale vypadalo to poněkud neohrabaně (Python 3.6 nám dal příjemnější syntaktickou strukturu):

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

Komentáře s typy navíc posloužily i pro podporu Pythonu 2, kde není vestavěná podpora typových anotací:

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

Ukázalo se, že tyto (a jiné) kompromisy ve skutečnosti neměly velký význam – výhody statické typizace vedly k tomu, že si uživatelé brzy zapomněli na nedokonalý syntaktický styl. Protože nyní v Python kódu, kde byly typy kontrolovány, nebyly používány speciální syntaktické konstrukce, stávající Python nástroje a procesy zpracování kódu pokračovaly v normálním fungování, což znatelně usnadnilo přijetí nového nástroje vývojáři.

Guido mě také přesvědčil, abych se připojil k Dropboxu poté, co jsem obhájil svou závěrečnou práci. Tím začíná to nejzajímavější v příběhu mypy.

Pokračování příště…

Vážení čtenáři! Pokud používáte Python, prosíme, podělte se s námi o to, jaké projekty tohoto rozsahu vyvíjíte v tomto jazyce.

Cesta k ověřování typů 4 milionů řádků kódu v Pythonu. Část 1
Cesta k ověřování typů 4 milionů řádků kódu v Pythonu. Část 1

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster