Dlaczego jest tak wiele Pythonów? Porównanie implementacji Pythona

Opublikowany: 2022-03-11

Python jest niesamowity.

Co zaskakujące, jest to dość niejednoznaczne stwierdzenie. Co mam na myśli mówiąc „Python”? Czy mam na myśli Pythona abstrakcyjny interfejs ? Czy mam na myśli CPython, powszechną implementację Pythona (nie mylić z Cythonem o podobnej nazwie)? Czy mam na myśli coś zupełnie innego? Może mam na myśli Jython, IronPython lub PyPy. A może naprawdę poszedłem na głęboką wodę i mówię o RPythonie lub RubyPythonie (które są bardzo, bardzo różnymi rzeczami).

Chociaż wyżej wymienione technologie są powszechnie nazywane i powszechnie przywoływane, niektóre z nich służą zupełnie innym celom (a przynajmniej działają na zupełnie inne sposoby).

Przez cały czas pracy z interfejsami Pythona natknąłem się na mnóstwo tych narzędzi .*ython. Ale do niedawna nie poświęciłem czasu, aby zrozumieć, czym one są, jak działają i dlaczego są konieczne (na swój własny sposób).

W tym samouczku zacznę od zera i przejdę przez różne implementacje Pythona, kończąc na dokładnym wprowadzeniu do PyPy, które moim zdaniem jest przyszłością języka.

Wszystko zaczyna się od zrozumienia, czym właściwie jest „Python”.

Jeśli dobrze rozumiesz kod maszynowy, maszyny wirtualne itp., możesz przejść dalej.

„Czy Python jest interpretowany lub kompilowany?”

Jest to powszechny problem dla początkujących w Pythonie.

Pierwszą rzeczą, którą należy sobie uświadomić podczas porównywania, jest to, że „Python” to interfejs . Istnieje specyfikacja tego, co Python powinien robić i jak powinien się zachowywać (jak w przypadku każdego interfejsu). I istnieje wiele implementacji (jak w przypadku każdego interfejsu).

Drugą rzeczą, którą należy sobie uświadomić, jest to, że „zinterpretowane” i „skompilowane” są właściwościami implementacji , a nie interfejsu .

Samo pytanie nie jest więc dobrze sformułowane.

Czy Python jest interpretowany czy kompilowany? Pytanie nie jest dobrze sformułowane.

To powiedziawszy, w przypadku najpopularniejszej implementacji Pythona (CPython: napisanej w C, często określanej po prostu jako „Python” i na pewno tego, czego używasz, jeśli nie masz pojęcia, o czym mówię), odpowiedź brzmi: zinterpretowane , z pewną kompilacją. CPython kompiluje * kod źródłowy Pythona do kodu bajtowego, a następnie interpretuje ten kod bajtowy, wykonując go w trakcie.

* Uwaga: nie jest to „kompilacja” w tradycyjnym znaczeniu tego słowa. Zazwyczaj powiedzielibyśmy, że „kompilacja” polega na przekształceniu języka wysokiego poziomu na kod maszynowy. Ale jest to swego rodzaju „kompilacja”.

Przyjrzyjmy się bliżej tej odpowiedzi, ponieważ pomoże nam to zrozumieć niektóre koncepcje, które pojawią się w dalszej części wpisu.

Kod bajtowy a kod maszynowy

Bardzo ważne jest zrozumienie różnicy między kodem bajtowym a kodem maszynowym (czyli kodem natywnym), być może najlepiej zilustrowanym na przykładzie:

  • C kompiluje się do kodu maszynowego, który jest następnie uruchamiany bezpośrednio na procesorze. Każda instrukcja instruuje procesor, aby przesuwał rzeczy.
  • Java kompiluje się do kodu bajtowego, który jest następnie uruchamiany na wirtualnej maszynie Java (JVM), która jest abstrakcją komputera wykonującego programy. Każda instrukcja jest następnie obsługiwana przez maszynę JVM, która współdziała z Twoim komputerem.

Krótko mówiąc: kod maszynowy jest znacznie szybszy, ale kod bajtowy jest bardziej przenośny i bezpieczny .

Kod maszynowy wygląda inaczej w zależności od komputera, ale kod bajtowy wygląda tak samo na wszystkich maszynach. Można powiedzieć, że kod maszynowy jest zoptymalizowany pod Twoją konfigurację.

Wracając do implementacji CPython, proces toolchain wygląda następująco:

  1. CPython kompiluje kod źródłowy Pythona do kodu bajtowego.
  2. Ten kod bajtowy jest następnie wykonywany na maszynie wirtualnej CPython.
Początkujący często zakładają, że Python jest kompilowany z powodu plików .pyc. Jest w tym trochę prawdy: plik .pyc to skompilowany kod bajtowy, który jest następnie interpretowany. Jeśli więc uruchamiałeś już swój kod w Pythonie i masz pod ręką plik .pyc, za drugim razem będzie on działał szybciej, ponieważ nie trzeba ponownie kompilować kodu bajtowego.

Alternatywne maszyny wirtualne: Jython, IronPython i inne

Jak wspomniałem wcześniej, Python ma kilka implementacji. Ponownie, jak wspomniano wcześniej, najczęstszym jest CPython, ale są też inne, o których należy wspomnieć ze względu na ten przewodnik porównawczy. Jest to implementacja Pythona napisana w C i uważana za implementację „domyślną”.

Ale co z alternatywnymi implementacjami Pythona? Jednym z bardziej znanych jest Jython, implementacja Pythona napisana w Javie, która wykorzystuje JVM. Podczas gdy CPython tworzy kod bajtowy do uruchomienia na maszynie wirtualnej CPython, Jython tworzy kod bajtowy Java do uruchomienia na maszynie JVM (to te same rzeczy, które są produkowane podczas kompilowania programu Java).

Użycie kodu bajtowego Javy przez Jythona jest przedstawione na diagramie implementacji Pythona.

„Dlaczego miałbyś kiedykolwiek skorzystać z alternatywnej implementacji?”, możesz zapytać. Po pierwsze, te różne implementacje Pythona dobrze współgrają z różnymi stosami technologii .

CPython bardzo ułatwia pisanie rozszerzeń C dla kodu Pythona, ponieważ w końcu jest on wykonywany przez interpreter C. Z drugiej strony Jython bardzo ułatwia pracę z innymi programami Java: możesz zaimportować dowolne klasy Java bez dodatkowego wysiłku, przywołując i wykorzystując swoje klasy Java z programów Jythona. (Poza tym: jeśli nie zastanowiłeś się nad tym dokładnie, to w rzeczywistości jest to szaleństwo. Jesteśmy w punkcie, w którym możesz mieszać i miksować różne języki i kompilować je wszystkie do tej samej substancji. (Jak wspomniał Rostin, programy, które mix Fortran i kod C są już od jakiegoś czasu.Więc oczywiście nie jest to koniecznie nowe.Ale nadal jest fajne.))

Jako przykład jest to prawidłowy kod Jythona:

 [Java HotSpot(TM) 64-Bit Server VM (Apple Inc.)] on java1.6.0_51 >>> from java.util import HashSet >>> s = HashSet(5) >>> s.add("Foo") >>> s.add("Bar") >>> s [Foo, Bar]

IronPython to kolejna popularna implementacja Pythona, napisana w całości w C# i skierowana do stosu .NET. W szczególności działa na czymś, co można nazwać wirtualną maszyną .NET, Microsoft Common Language Runtime (CLR), porównywalnym z JVM.

Można powiedzieć, że Jython:Java ::IronPython:C# . Działają na tych samych odpowiednich maszynach wirtualnych, możesz importować klasy C# z kodu IronPython i klasy Java z kodu Jython itp.

Całkowicie możliwe jest przetrwanie bez dotykania implementacji Pythona innej niż CPython. Jednak przełączanie ma swoje zalety, z których większość zależy od posiadanego stosu technologicznego. Używasz wielu języków opartych na JVM? Jython może być dla ciebie. Wszystko o stosie .NET? Może powinieneś spróbować IronPythona (a może już masz).

Ta tabela porównawcza Pythona pokazuje różnice między implementacjami Pythona.

Przy okazji: chociaż nie byłby to powód do używania innej implementacji, zauważ, że te implementacje różnią się zachowaniem poza tym, jak traktują twój kod źródłowy Pythona. Jednak różnice te są zazwyczaj niewielkie i z czasem zanikają lub pojawiają się, gdy te implementacje są aktywnie rozwijane. Na przykład IronPython domyślnie używa ciągów Unicode; Jednak CPython domyślnie używa ASCII dla wersji 2.x (niepowodzenie z UnicodeEncodeError dla znaków spoza ASCII), ale domyślnie obsługuje ciągi Unicode dla 3.x.

Kompilacja Just-in-Time: PyPy i przyszłość

Mamy więc implementację Pythona napisaną w C, jedną w Javie i jedną w C#. Kolejny logiczny krok: implementacja Pythona napisana w… Pythonie. (Wykształcony czytelnik zauważy, że jest to nieco mylące.)

Oto, gdzie sprawy mogą być zagmatwane. Najpierw omówmy kompilację just-in-time (JIT).

JIT: Dlaczego i jak?

Przypomnij sobie, że natywny kod maszynowy jest znacznie szybszy niż kod bajtowy. A co by było, gdybyśmy mogli skompilować część naszego kodu bajtowego, a następnie uruchomić go jako kod natywny? Musielibyśmy zapłacić pewną cenę za skompilowanie kodu bajtowego (tj. czas), ale gdyby wynik końcowy był szybszy, byłoby świetnie! To jest motywacja kompilacji JIT, hybrydowej techniki, która łączy zalety interpreterów i kompilatorów. Zasadniczo JIT chce wykorzystać kompilację do przyspieszenia interpretowanego systemu.

Na przykład wspólne podejście przyjęte przez JIT:

  1. Zidentyfikuj kod bajtowy, który jest często wykonywany.
  2. Skompiluj go do natywnego kodu maszynowego.
  3. Buforuj wynik.
  4. Ilekroć ten sam kod bajtowy jest ustawiony do uruchomienia, zamiast tego pobierz wstępnie skompilowany kod maszynowy i czerp korzyści (tj. zwiększenie prędkości).

Na tym właśnie polega implementacja PyPy: przeniesienie JIT do Pythona (poprzednie działania można znaleźć w dodatku ). Są oczywiście inne cele: PyPy ma być wieloplatformowy, lekki i wspierający bez stosu. Ale JIT jest tak naprawdę jego punktem sprzedaży. Jako średnia z wielu testów czasowych, mówi się, że poprawia wydajność o współczynnik 6,27. Aby zobaczyć podział, zobacz ten wykres z PyPy Speed ​​Center:

Przeniesienie JIT do interfejsu Pythona za pomocą implementacji PyPy opłaca się w poprawie wydajności.

Trudno zrozumieć PyPy

PyPy ma ogromny potencjał iw tym momencie jest wysoce kompatybilny z CPythonem (więc może uruchamiać Flask, Django itp.).

Ale wokół PyPy jest dużo zamieszania (patrz na przykład ta bezsensowna propozycja stworzenia PyPy…). Moim zdaniem to przede wszystkim dlatego, że PyPy to tak naprawdę dwie rzeczy:

  1. Interpreter Pythona napisany w RPythonie (nie Pythonie (kłamałem wcześniej)). RPython to podzbiór Pythona ze statycznym typowaniem. W Pythonie „przeważnie niemożliwe” jest rygorystyczne wnioskowanie o typach (dlaczego to takie trudne? Rozważmy fakt, że:

     x = random.choice([1, "foo"])

    byłby poprawnym kodem Pythona (podziękowanie dla Ademana). Jaki jest typ x ? Jak możemy wnioskować o typach zmiennych, gdy typy nie są nawet ściśle narzucane?). Dzięki RPythonowi poświęcasz pewną elastyczność, ale zamiast tego znacznie, znacznie łatwiej jest myśleć o zarządzaniu pamięcią i tak dalej, co pozwala na optymalizacje.

  2. Kompilator, który kompiluje kod RPython dla różnych celów i dodaje JIT. Domyślną platformą jest C, tj. kompilator RPython-to-C, ale możesz także atakować JVM i inne.

Wyłącznie dla jasności w tym przewodniku porównującym Pythona będę nazywać je PyPy (1) i PyPy (2).

Po co ci te dwie rzeczy i dlaczego pod jednym dachem? Pomyśl o tym w ten sposób: PyPy (1) to interpreter napisany w RPythonie. Więc pobiera kod Pythona użytkownika i kompiluje go do kodu bajtowego. Ale sam interpreter (napisany w RPythonie) musi być zinterpretowany przez inną implementację Pythona, aby mógł działać, prawda?

Cóż, moglibyśmy po prostu użyć CPythona do uruchomienia interpretera. Ale to nie byłoby zbyt szybkie.

Zamiast tego chodzi o to, że używamy PyPy (2) (nazywanego RPython Toolchain) do kompilacji interpretera PyPy do kodu dla innej platformy (np. C, JVM lub CLI) do uruchomienia na naszej maszynie, dodając JIT jako dobrze. To magiczne: PyPy dynamicznie dodaje JIT do interpretera, generując własny kompilator! ( Znowu to jest szaleństwo: kompilujemy interpreter, dodając kolejny osobny, samodzielny kompilator. )

Rezultatem jest samodzielny plik wykonywalny, który interpretuje kod źródłowy Pythona i wykorzystuje optymalizacje JIT. Właśnie tego chcieliśmy! To kęs, ale może ten schemat pomoże:

Ten diagram ilustruje piękno implementacji PyPy, w tym interpretera, kompilatora i pliku wykonywalnego z JIT.

Powtarzając, prawdziwe piękno PyPy polega na tym, że moglibyśmy napisać sobie kilka różnych interpreterów Pythona w RPythonie, nie martwiąc się o JIT. PyPy następnie zaimplementowałby dla nas JIT za pomocą RPython Toolchain/PyPy (2).

W rzeczywistości, jeśli otrzymamy jeszcze bardziej abstrakcyjny, możesz teoretycznie napisać tłumacza dla dowolnego języka, przesłać go do PyPy i uzyskać JIT dla tego języka. Dzieje się tak, ponieważ PyPy skupia się na optymalizacji samego tłumacza, a nie na szczegółach tłumaczonego języka.

Możesz teoretycznie napisać tłumacza dla dowolnego języka, nakarmić nim PyPy i uzyskać JIT dla tego języka.

W ramach krótkiej dygresji nadmienię, że sam JIT jest absolutnie fascynujący. Wykorzystuje technikę zwaną śledzeniem, która działa w następujący sposób:

  1. Uruchom interpreter i zinterpretuj wszystko (bez dodawania JIT).
  2. Wykonaj lekkie profilowanie interpretowanego kodu.
  3. Zidentyfikuj operacje, które wykonałeś wcześniej.
  4. Skompiluj te bity kodu do kodu maszynowego.

Co więcej, ten artykuł jest bardzo przystępny i bardzo interesujący.

Podsumowując: używamy kompilatora PyPy RPython-to-C (lub innej platformy docelowej) do kompilacji interpretera zaimplementowanego w RPython PyPy.

Zawijanie

Po długim porównaniu implementacji Pythona muszę zadać sobie pytanie: dlaczego to jest takie świetne? Dlaczego warto realizować ten szalony pomysł? Myślę, że Alex Gaynor dobrze to ujął na swoim blogu: „[PyPy to przyszłość], ponieważ oferuje lepszą szybkość, większą elastyczność i jest lepszą platformą rozwoju Pythona”.

W skrócie:

  • Jest szybki, ponieważ kompiluje kod źródłowy do kodu natywnego (przy użyciu JIT).
  • Jest elastyczny, ponieważ dodaje JIT do tłumacza przy bardzo niewielkiej dodatkowej pracy.
  • Jest elastyczny (znowu), ponieważ możesz pisać swoje interpretery w RPythonie , który jest łatwiejszy do rozszerzenia niż powiedzmy C (w rzeczywistości jest to tak proste, że jest samouczek do pisania własnych interpreterów).

Dodatek: Inne nazwy Pythona, które mogłeś słyszeć

  • Python 3000 (Py3k): alternatywne nazewnictwo dla Pythona 3.0, ważnego, niezgodnego wstecz wydania Pythona, które weszło na scenę w 2008 roku. Zespół Py3k przewidział, że pełne przyjęcie nowej wersji zajmie około pięciu lat. I chociaż większość (ostrzeżenie: anegdotyczne twierdzenie) programistów Pythona nadal używa Pythona 2.x, ludzie są coraz bardziej świadomi Py3k.

  • Cython: nadzbiór Pythona, który zawiera wiązania do wywoływania funkcji C.
    • Cel: umożliwienie pisania rozszerzeń w C dla kodu Pythona.
    • Umożliwia także dodanie statycznego pisania do istniejącego kodu Pythona, co pozwala na jego kompilację i osiągnięcie wydajności zbliżonej do C.
    • Jest to podobne do PyPy, ale nie takie samo. W takim przypadku wymuszasz wpisywanie kodu użytkownika przed przekazaniem go do kompilatora. Za pomocą PyPy piszesz zwykły stary Python, a kompilator obsługuje wszelkie optymalizacje.

  • Numba: „specjalistyczny kompilator just-in-time”, który dodaje JIT do kodu Pythona z adnotacjami . Mówiąc najprościej, dajesz mu kilka wskazówek, a to przyspiesza fragmenty kodu. Numba jest częścią dystrybucji Anaconda, zestawu pakietów do analizy i zarządzania danymi.

  • IPython: bardzo różni się od wszystkiego, co zostało omówione. Środowisko obliczeniowe dla Pythona. Interaktywny z obsługą zestawów narzędzi GUI i przeglądarki itp.

  • Psyco: moduł rozszerzeń Pythona i jedna z wczesnych prób JIT w Pythonie. Jednak od tego czasu został oznaczony jako „nieutrzymywany i martwy”. W rzeczywistości główny programista Psyco, Armin Rigo, pracuje teraz nad PyPy.

Wiązania języka Python

  • RubyPython: pomost między maszynami wirtualnymi Ruby i Python. Pozwala na osadzenie kodu Pythona w kodzie Rubiego. Definiujesz, gdzie Python zaczyna i kończy, a RubyPython organizuje dane między maszynami wirtualnymi.

  • PyObjc: powiązania językowe między Pythonem a Objective-C, działające jako pomost między nimi. W praktyce oznacza to, że możesz korzystać z bibliotek Objective-C (w tym wszystkiego, czego potrzebujesz do tworzenia aplikacji OS X) z kodu Pythona i modułów Pythona z kodu Objective-C. W tym przypadku wygodnie jest, że CPython jest napisany w C, który jest podzbiorem Objective-C.

  • PyQt: podczas gdy PyObjc zapewnia wiązanie dla komponentów GUI OS X, PyQt robi to samo dla frameworka aplikacji Qt, umożliwiając tworzenie bogatych interfejsów graficznych, dostęp do baz danych SQL itp. Kolejne narzędzie mające na celu wprowadzenie prostoty Pythona do innych frameworków.

Ramy JavaScript

  • pyjs (Pyjamas): framework do tworzenia aplikacji internetowych i desktopowych w Pythonie. Zawiera kompilator Python-to-JavaScript, zestaw widżetów i kilka innych narzędzi.

  • Brython: maszyna wirtualna Pythona napisana w JavaScript, aby umożliwić wykonanie kodu Py3k w przeglądarce.