Droga do weryfikacji typów 4 milionów linii kodu Python. Część 1

Dziś oferujemy Państwu pierwszą część tłumaczenia materiału na temat tego, jak w Dropboxie zajmujemy się kontrolą typów w kodzie Python.

Droga do weryfikacji typów 4 milionów linii kodu Python. Część 1

W Dropboxie wiele pisań odbywa się w Pythonie. To język, którego używamy niezwykle szeroko – zarówno do backendowych usług, jak i do aplikacji klienckich na komputery stacjonarne. Ponadto w dużych ilościach korzystamy z Go, TypeScript i Rust, ale Python jest naszym głównym językiem. Biorąc pod uwagę nasze rozmiary, mówimy o milionach linii kodu w Pythonie, okazało się, że dynamiczna typizacja tego kodu niepotrzebnie komplikuje jego zrozumienie i zaczyna poważnie wpływać na wydajność pracy. Aby złagodzić ten problem, zaczęliśmy stopniową transformację naszego kodu na statyczną kontrolę typów przy użyciu mypy. To prawdopodobnie najbardziej popularny samodzielny system sprawdzania typów dla Pythona. Mypy to projekt open-source, w którym główni deweloperzy pracują w Dropboxie.

Dropbox był jedną z pierwszych firm, które wdrożyły statyczną kontrolę typów w kodzie Python na taką skalę. W dzisiejszych czasach mypy jest używane w tysiącach projektów. Narzędzie to zostało przetestowane w praktyce niezliczoną ilość razy. Aby dojść do miejsca, w którym się teraz znajdujemy, przeszliśmy długą drogę. Na tej drodze mieliśmy wiele nieudanych prób i niepowodzeń eksperymentów. Ten materiał opowiada o historii statycznej kontroli typów w Pythonie – od jej trudnego początku, który był częścią mojego projektu badawczego, do dzisiejszego dnia, kiedy kontrole typów i sugestie typowe stały się codziennością dla niezliczonej liczby deweloperów piszących w Pythonie. Mechanizmy te są teraz wspierane przez wiele narzędzi – takich jak IDE i analizatory kodu.

Przeczytaj drugą część

Po co potrzebna jest kontrola typów?

Jeśli kiedykolwiek korzystałeś z dynamicznie typowanego Pythona, możesz mieć pewne wątpliwości co do tego, dlaczego wokół statycznej typizacji i mypy ostatnio powstał taki hałas. Może nawet tak jest, że Python podoba ci się właśnie ze względu na jego dynamiczną typizację, a to, co się dzieje, po prostu cię frustruje. Kluczem do wartości statycznej typizacji jest skala rozwiązań: im większy jest twój projekt, tym bardziej skłaniasz się ku statycznej typizacji, a w końcu tym bardziej staje się to naprawdę potrzebne.

Załóżmy, że pewien projekt osiągnął rozmiar dziesiątek tysięcy wierszy kodu i okazało się, że pracuje nad nim kilku programistów. Analizując podobny projekt, możemy na podstawie naszego doświadczenia powiedzieć, że zrozumienie jego kodu będzie kluczem do utrzymania produktywności programistów. Bez adnotacji typów czasami trudno jest określić, na przykład, jakie argumenty należy przekazać do funkcji, lub jakie typy mogą być zwracane przez daną funkcję. Oto typowe pytania, na które często trudno odpowiedzieć bez stosowania adnotacji typów:

  • Czy ta funkcja może zwrócić Brak?
  • Czym powinien być ten argument elementy?
  • Jaki jest typ atrybutu id: int czy jest to str, czy może jakiś typ użytkownika?
  • Czy ten argument powinien być listą? Czy można przekazać do niego krotkę?

Jeśli spojrzysz na następujący fragment kodu, wyposażony w adnotacje typów, i spróbujesz odpowiedzieć na podobne pytania, okaże się, że to bardzo proste zadanie:

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

  • read_metadata nie zwraca Brak, ponieważ zwracany typ nie jest Optional[…].
  • Argument elementy — to sekwencja napisów. Nie można jej iterować w dowolnej kolejności.
  • Atrybut id — to ciąg bajtów.

W idealnym świecie można by oczekiwać, że wszystkie tego typu szczegóły będą opisane w dokumentacji wbudowanej (docstring). Jednak doświadczenie pokazuje, że często w kodzie, z którym przychodzi nam pracować, nie ma takiej dokumentacji. Nawet jeśli taka dokumentacja istnieje w kodzie, nie można liczyć na jej całkowitą poprawność. Ta dokumentacja może być niejasna, niedokładna, pozostawiająca wiele możliwości do jej błędnego zrozumienia. W dużych zespołach lub projektach ta problematyka może stać się niezwykle ostra.

Choć Python doskonale sprawdza się na wczesnych lub pośrednich etapach projektów, w pewnym momencie udane projekty i firmy używające Pythona mogą stanąć przed kluczowym pytaniem: „Czy powinniśmy przepisać wszystko w statycznie typowanym języku?”.

Systemy sprawdzania typów, takie jak mypy, rozwiązują powyższą kwestię, oferując programistom formalny język do opisywania typów oraz sprawdzając, czy opisy typów odpowiadają implementacjom programów (a co nie jest konieczne, weryfikują ich istnienie). Ogólnie można powiedzieć, że te systemy dają nam coś w rodzaju starannie zweryfikowanej dokumentacji.

Zastosowanie takich systemów ma też inne zalety, które są już całkiem istotne:

  • System sprawdzania typów może wykryć niektóre drobne (a także — nieco większe) błędy. Typowy przykład to zapomnienie o obsłużeniu wartości Brak lub jakiegoś innego warunku szczególnego.
  • Refaktoryzacja kodu staje się znacznie prostsza, ponieważ system sprawdzania typów często bardzo dokładnie informuje, jaki kod należy zmienić. Nie musimy liczyć na 100% pokrycie kodu testami, co w każdym razie zazwyczaj jest niewykonalne. Nie musimy zgłębiać raportów z śladów stosu, aby ustalić przyczynę problemu.
  • Nawet w dużych projektach mypy często potrafi przeprowadzić pełną kontrolę typów w ułamku sekundy. Wykonywanie testów zwykle zajmuje kilkanaście sekund, a nawet minuty. System sprawdzania typów daje programiście natychmiastową informację zwrotną i pozwala mu szybciej wykonywać swoją pracę. Nie musi już pisać kruchych i trudnych w utrzymaniu testów jednostkowych, które zastępują prawdziwe encje mokami i patchami tylko po to, aby szybciej uzyskać wyniki testów kodu.

IDE i edytory, takie jak PyCharm lub Visual Studio Code, wykorzystują możliwości adnotacji typów do zapewnienia programistom możliwości automatycznego uzupełniania kodu, podświetlania błędów oraz wsparcia dla często używanych konstrukcji językowych. I to tylko niektóre z korzyści, jakie niesie ze sobą typizacja. Dla niektórych programistów to wszystko jest głównym argumentem na rzecz typizacji. To, co przynosi korzyści od razu po wdrożeniu w pracy. Ten sposób użycia typów nie wymaga stosowania oddzielnego systemu sprawdzania typów, takiego jak mypy, chociaż warto zauważyć, że mypy pomaga utrzymać zgodność adnotacji typów z kodem.

Historia mypy

Historia mypy zaczęła się w Wielkiej Brytanii, w Cambridge, kilka lat przed tym, jak dołączyłem do Dropbox. Zajmowałem się, w ramach pracy doktorskiej, kwestią unifikacji języków statycznie typowanych i dynamicznych. Inspirował mnie artykuł o stopniowej typizacji autorstwa Jeremy'ego Sieka i Walida Taha, a także projekt Typed Racket. Starałem się znaleźć sposoby użycia tego samego języka programowania do różnych projektów — od małych skryptów po bazy kodu składające się z milionów linii. Przy tym zależało mi, aby w projektach dowolnej skali nie trzeba było podejmować zbyt dużych kompromisów. Ważną częścią tego wszystkiego była idea stopniowego przechodzenia od prototypu projektu bez typów do w pełni przetestowanego, statycznie typowanego gotowego produktu. W dzisiejszych czasach te pomysły są w dużej mierze traktowane jako coś oczywistego, ale w 2010 roku był to problem, który wciąż był aktywnie badany.

Moja pierwotna praca w zakresie sprawdzania typów nie była skierowana na Pythona. Zamiast tego używałem małego „domowego” języka AloreOto przykład, który pozwoli Ci zrozumieć, o co chodzi (adnotacje typów tutaj nie są obowiązkowe):

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

Użycie uproszczonego języka zaprojektowanego samodzielnie to powszechny sposób w badaniach naukowych. Dzieje się tak między innymi dlatego, że umożliwia to szybkie przeprowadzanie eksperymentów oraz dlatego, że można swobodnie ignorować to, co nie ma związku z badaniem. Rzeczywiste języki programowania są zazwyczaj złożonymi zjawiskami z trudnymi implementacjami, co spowalnia eksperymenty. Niemniej jednak jakiekolwiek wyniki oparte na uproszczonym języku mogą wydawać się nieco podejrzane, ponieważ przy ich uzyskiwaniu badacz mógł zrezygnować z rozważań istotnych dla praktycznego zastosowania języków.

Moje narzędzie do sprawdzania typów dla Alore wydawało się bardzo obiecujące, ale chciałem je przetestować, wykonując eksperymenty z rzeczywistym kodem, który, można powiedzieć, nie był napisany w Alore. Na moje szczęście język Alore w dużej mierze oparty był na tych samych pomysłach co Python. Było wystarczająco łatwo przerobić narzędzie do sprawdzania typów, aby mogło działać z składnią i semantyką Pythona. To umożliwiło mi próbę przeprowadzenia walidacji typów w otwartoźródłowym kodzie Pythona. Ponadto napisałem transpiler do konwersji kodu napisanego w Alore na kod Python i użyłem go do przetłumaczenia kodu mojego narzędzia do sprawdzania typów. Teraz miałem system do sprawdzania typów, napisany w Pythonie, który wspierał podzbiór Pythona, pewnego rodzaju odmianę tego języka! (Niektóre rozwiązania architektoniczne, które miały sens w Alore, nie pasowały do Pythona, co wciąż jest zauważalne w niektórych częściach bazy kodu mypy.)

Tak naprawdę, język wspierany przez mój system typów w tym momencie nie do końca można było nazwać Pythonem: był to wariant Pythona z powodu pewnych ograniczeń składni adnotacji typów 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 tym czasie było wykorzystanie adnotacji typów do poprawy wydajności poprzez kompilację tej odmiany Pythona do C, lub być może do bajt-kodu JVM. Zrobiłem postępy w pisaniu prototypu kompilatora, ale porzuciłem ten pomysł, ponieważ sama weryfikacja typów wydawała się wystarczająco przydatna.

Ostatecznie zaprezentowałem mój projekt na konferencji PyCon 2013 w Santa Clara. Rozmawiałem o tym także z Guido van Rossumem, wielkodusznym dożywotnim dyktatorem Pythona. Udało mu się przekonać mnie, abym zrezygnował z własnej składni i trzymał się standardowej składni Pythona 3. Python 3 wspiera adnotacje funkcji, w wyniku czego mój przykład mógł być przepisany w sposób pokazany poniżej, uzyskując 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 muszę zaznaczyć, że wymyśliłem własną składnię dlatego). W szczególności Python 3.3, najnowsza wersja języka w tamtym czasie, nie wspierał adnotacji zmiennych. Omówiłem z Guido przez e-mail różne możliwości składniowych formatów takich adnotacji. Zdecydowaliśmy się użyć komentarzy z oznaczeniem typów dla zmiennych. Pozwoliło to na osiągnięcie zamierzonego celu, ale wyglądało dość nieporęcznie (Python 3.6 dostarczył nam ładniejszej składni):

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

Komentarze z typami były również przydatne do wsparcia Pythona 2, który nie ma wbudowanej obsługi adnotacji typów:

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 większego znaczenia — zalety statycznej typizacji sprawiły, że użytkownicy szybko zapomnieli o nie do końca idealnej składni. Ponieważ w kodzie Pythona, w którym kontrolowano typy, nie stosowano specjalnych konstrukcji składniowych, istniejące narzędzia i procesy Pythona mogły dalej działać normalnie, co znacznie ułatwiło programistom naukę nowego narzędzia.

Guido również przekonał mnie do dołączenia do Dropbox po tym, jak obroniłem pracę dyplomową. To tutaj zaczyna się najciekawsza część historii mypy.

Ciąg dalszy nastąpi…

Drodzy Czytelnicy! Jeśli korzystasz z Pythona, opowiedz nam, jakie projekty realizujesz w tym języku.

Droga do weryfikacji typów 4 milionów linii kodu Python. Część 1
Droga do weryfikacji typów 4 milionów linii kodu Python. Część 1

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster