Ścieżka do sprawdzania typów 4 milionów linii kodu Pythona. Część 1

Dziś zwracamy uwagę na pierwszą część tłumaczenia materiału o tym, jak Dropbox radzi sobie z kontrolą typu kodu Pythona.

Ścieżka do sprawdzania typów 4 milionów linii kodu Pythona. Część 1

Dropbox dużo pisze w Pythonie. Jest to język, którego używamy niezwykle szeroko, zarówno w przypadku usług zaplecza, jak i aplikacji klienckich na komputery stacjonarne. Często używamy Go, TypeScript i Rust, ale Python jest naszym głównym językiem. Biorąc pod uwagę naszą skalę, a mówimy o milionach linii kodu Pythona, okazało się, że dynamiczne wpisywanie takiego kodu niepotrzebnie komplikuje jego zrozumienie i zaczęło poważnie wpływać na wydajność pracy. Aby złagodzić ten problem, zaczęliśmy stopniowo przenosić nasz kod na statyczne sprawdzanie typów za pomocą mypy. Jest to prawdopodobnie najpopularniejszy samodzielny system sprawdzania typów w Pythonie. Mypy to projekt typu open source, którego główni programiści pracują w Dropbox.

Dropbox był jedną z pierwszych firm, które wdrożyły statyczne sprawdzanie typów w kodzie Pythona na taką skalę. Mypy jest obecnie używany w tysiącach projektów. To narzędzie niezliczoną ilość razy, jak mówią, „przetestowane w bitwie”. Przeszliśmy długą drogę, aby znaleźć się w miejscu, w którym jesteśmy teraz. Po drodze było wiele nieudanych przedsięwzięć i nieudanych eksperymentów. Ten post obejmuje historię statycznego sprawdzania typów w Pythonie, od jego trudnych początków w ramach mojego projektu badawczego, do dnia dzisiejszego, kiedy sprawdzanie typów i podpowiadanie typów stało się powszechne dla niezliczonych programistów piszących w Pythonie. Mechanizmy te są obecnie obsługiwane przez wiele narzędzi, takich jak IDE i analizatory kodu.

Przeczytaj drugą część

Dlaczego sprawdzanie typu jest konieczne?

Jeśli kiedykolwiek używałeś Pythona z dynamicznym typowaniem, możesz mieć pewne wątpliwości, dlaczego ostatnio było takie zamieszanie wokół statycznego pisania i mypy. A może lubisz Pythona właśnie ze względu na jego dynamiczne pisanie, a to, co się dzieje, po prostu cię denerwuje. Kluczem do wartości pisania statycznego jest skala rozwiązań: im większy projekt, tym bardziej skłaniasz się ku statycznemu typowaniu, a ostatecznie tym bardziej go naprawdę potrzebujesz.

Załóżmy, że pewien projekt osiągnął rozmiar kilkudziesięciu tysięcy linii i okazało się, że pracuje nad nim kilku programistów. Patrząc na podobny projekt, na podstawie naszych doświadczeń, możemy powiedzieć, że zrozumienie jego kodu będzie kluczem do utrzymania produktywności programistów. Bez adnotacji typu może być trudno ustalić, na przykład, jakie argumenty przekazać funkcji lub jakie typy może zwrócić funkcja. Oto typowe pytania, na które często trudno jest odpowiedzieć bez użycia adnotacji typu:

  • Czy ta funkcja może powrócić None?
  • Jaki powinien być ten argument? items?
  • Jaki jest typ atrybutu id: int czy to jest, str, a może jakiś niestandardowy typ?
  • Czy ten argument powinien być listą? Czy można przekazać do niego krotkę?

Jeśli spojrzysz na poniższy fragment kodu z adnotacją typu i spróbujesz odpowiedzieć na podobne pytania, okaże się, że jest to najprostsze zadanie:

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

  • read_metadata nie wraca None, ponieważ zwracany typ nie jest Optional[…].
  • argument items jest ciągiem linii. Nie można go powtarzać losowo.
  • Atrybut id jest ciągiem bajtów.

W idealnym świecie można by oczekiwać, że wszystkie takie subtelności zostaną opisane we wbudowanej dokumentacji (docstring). Ale doświadczenie daje wiele przykładów na to, że często takiej dokumentacji nie obserwuje się w kodzie, z którym trzeba pracować. Nawet jeśli taka dokumentacja jest obecna w kodzie, nie można liczyć na jej absolutną poprawność. Ta dokumentacja może być niejasna, niedokładna i narażona na nieporozumienia. W dużych zespołach lub dużych projektach problem ten może stać się niezwykle dotkliwy.

Podczas gdy Python doskonale sprawdza się na wczesnych lub pośrednich etapach projektów, w pewnym momencie udane projekty i firmy korzystające z Pythona mogą stanąć przed kluczowym pytaniem: „Czy powinniśmy przepisać wszystko w języku o typie statycznym?”.

Systemy sprawdzania typów, takie jak mypy, rozwiązują powyższy problem, dostarczając programiście formalny język do opisywania typów oraz sprawdzając, czy deklaracje typów pasują do implementacji programu (i opcjonalnie sprawdzając, czy istnieją). Ogólnie można powiedzieć, że systemy te oddają do naszej dyspozycji coś w rodzaju dokładnie sprawdzonej dokumentacji.

Stosowanie takich systemów ma jeszcze inne zalety, a są one już zupełnie niebanalne:

  • System sprawdzania typów może wykryć drobne (i nie takie małe) błędy. Typowym przykładem jest sytuacja, w której zapominają przetworzyć wartość None lub inny specjalny warunek.
  • Refaktoryzacja kodu jest znacznie uproszczona, ponieważ system sprawdzania typów często bardzo dokładnie określa, jaki kod należy zmienić. Jednocześnie nie musimy liczyć na 100% pokrycie kodu testami, co i tak zazwyczaj jest niewykonalne. Nie musimy zagłębiać się w szczegóły śledzenia stosu, aby znaleźć przyczynę problemu.
  • Nawet w przypadku dużych projektów mypy często może wykonać pełne sprawdzenie typu w ułamku sekundy. A wykonanie testów zajmuje zwykle kilkadziesiąt sekund, a nawet minut. System sprawdzania typu daje programiście natychmiastową informację zwrotną i pozwala mu szybciej wykonać swoją pracę. Nie musi już pisać delikatnych i trudnych w utrzymaniu testów jednostkowych, które zastępują prawdziwe byty makietami i łatami, aby szybciej uzyskać wyniki testów kodu.

IDE i edytory, takie jak PyCharm lub Visual Studio Code, wykorzystują moc adnotacji typu, aby zapewnić programistom uzupełnianie kodu, wyróżnianie błędów i obsługę powszechnie używanych konstrukcji językowych. A to tylko niektóre z zalet pisania na klawiaturze. Dla niektórych programistów to wszystko jest głównym argumentem przemawiającym za pisaniem na klawiaturze. Jest to coś, co przynosi korzyści natychmiast po wdrożeniu. Ten przypadek użycia typów nie wymaga oddzielnego systemu sprawdzania typów, takiego jak mypy, chociaż należy zauważyć, że mypy pomaga zachować spójność adnotacji typu z kodem.

Tło mypy

Historia mypy zaczęła się w Wielkiej Brytanii, w Cambridge, na kilka lat przed dołączeniem do Dropbox. W ramach pracy doktorskiej zajmowałem się unifikacją języków typowanych statycznie i języków dynamicznych. Zainspirował mnie artykuł Jeremy'ego Sieka i Walida Taha o pisaniu przyrostowym oraz projekt Typed Racket. Próbowałem znaleźć sposoby na wykorzystanie tego samego języka programowania do różnych projektów – od małych skryptów po bazy kodu składające się z wielu milionów linii. Jednocześnie chciałem mieć pewność, że w projekcie dowolnej skali nie trzeba będzie iść na zbyt duże kompromisy. Ważną częścią tego wszystkiego była idea stopniowego przechodzenia od nieopisanego projektu prototypu do kompleksowo przetestowanego statycznie typowanego gotowego produktu. Obecnie te pomysły są w dużej mierze uważane za oczywiste, ale w 2010 roku był to problem, który wciąż był aktywnie badany.

Moja oryginalna praca nad sprawdzaniem typów nie była skierowana do Pythona. Zamiast tego użyłem małego „domowego” języka Alor. Oto przykład, który pozwoli ci zrozumieć, o czym mówimy (adnotacje typu są tutaj opcjonalne):

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

Używanie uproszczonego języka ojczystego jest powszechnym podejściem stosowanym w badaniach naukowych. Dzieje się tak nie tylko dlatego, że pozwala szybko przeprowadzać eksperymenty, ale także dlatego, że to, co nie ma nic wspólnego z badaniami, można łatwo zignorować. Rzeczywiste języki programowania są zwykle zjawiskami na dużą skalę ze złożonymi implementacjami, co spowalnia eksperymentowanie. Jednak wszelkie wyniki oparte na uproszczonym języku wyglądają nieco podejrzanie, ponieważ uzyskując te wyniki, badacz mógł poświęcić względy ważne dla praktycznego użycia języków.

Mój moduł sprawdzania typów dla Alore wyglądał bardzo obiecująco, ale chciałem go przetestować, eksperymentując z prawdziwym kodem, który, można powiedzieć, nie został napisany w Alore. Na szczęście dla mnie język Alore był w dużej mierze oparty na tych samych pomysłach co Python. Zmiana modułu sprawdzania typów była dość łatwa, tak aby działał ze składnią i semantyką Pythona. To pozwoliło nam wypróbować sprawdzanie typów w otwartym kodzie Pythona. Ponadto napisałem transpiler do konwersji kodu napisanego w Alore na kod Pythona i użyłem go do przetłumaczenia mojego kodu sprawdzania typów. Teraz miałem system sprawdzania typów napisany w Pythonie, który obsługiwał podzbiór Pythona, jakiś rodzaj tego języka! (Niektóre decyzje architektoniczne, które miały sens dla Alore, były słabo dopasowane do Pythona i nadal jest to zauważalne w niektórych częściach bazy kodu mypy).

W rzeczywistości język obsługiwany przez mój system typów nie mógł być w tym momencie nazwany Pythonem: był to wariant Pythona ze względu na pewne ograniczenia składni adnotacji typu Pythona 3.

Wyglądało to jak mieszanka Javy i Pythona:

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

Jednym z moich pomysłów w tamtym czasie było użycie adnotacji typu w celu poprawy wydajności poprzez kompilację tego rodzaju Pythona do C lub być może kodu bajtowego JVM. Doszedłem do etapu pisania prototypu kompilatora, ale porzuciłem ten pomysł, ponieważ samo sprawdzanie typów wydawało się całkiem przydatne.

Skończyło się na tym, że zaprezentowałem swój projekt na PyCon 2013 w Santa Clara. Rozmawiałem też o tym z Guido van Rossumem, życzliwym dożywotnim dyktatorem Pythona. Przekonał mnie, abym porzucił własną składnię i pozostał przy standardowej składni Pythona 3. Python 3 obsługuje adnotacje funkcji, więc mój przykład można przepisać tak, jak pokazano poniżej, w wyniku czego otrzymamy normalny program w Pythonie:

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

Musiałem pójść na pewne kompromisy (przede wszystkim chcę zauważyć, że właśnie z tego powodu wymyśliłem własną składnię). W szczególności Python 3.3, najnowsza wersja języka w tamtym czasie, nie obsługiwał adnotacji zmiennych. Omówiłem z Guido przez e-mail różne możliwości projektowania składniowego takich adnotacji. Zdecydowaliśmy się użyć komentarzy typu dla zmiennych. Służyło to zamierzonemu celowi, ale było nieco kłopotliwe (Python 3.6 dał nam ładniejszą składnię):

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

Komentarze typu przydały się również do obsługi Pythona 2, który nie ma wbudowanej obsługi adnotacji typu:

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

Okazało się, że te (i inne) kompromisy tak naprawdę nie miały znaczenia - korzyści płynące ze statycznego pisania sprawiły, że użytkownicy szybko zapomnieli o mniej niż idealnej składni. Ponieważ w sprawdzanym typie kodu Pythona nie użyto żadnych specjalnych konstrukcji składniowych, istniejące narzędzia Pythona i procesy przetwarzania kodu nadal działały normalnie, co znacznie ułatwiło programistom naukę nowego narzędzia.

Guido przekonał mnie również do dołączenia do Dropbox po ukończeniu pracy magisterskiej. Tu zaczyna się najciekawsza część historii mypy.

To be continued ...

Drodzy Czytelnicy! Jeśli używasz Pythona, opowiedz nam o skali projektów, które rozwijasz w tym języku.

Ścieżka do sprawdzania typów 4 milionów linii kodu Pythona. Część 1
Ścieżka do sprawdzania typów 4 milionów linii kodu Pythona. Część 1

Źródło: www.habr.com

Dodaj komentarz