De ce sunt atât de mulți pitoni? O comparație de implementare Python
Publicat: 2022-03-11Python este uimitor.
În mod surprinzător, aceasta este o afirmație destul de ambiguă. Ce vreau să spun prin „Python”? Mă refer la Python, interfața abstractă? Mă refer la CPython, implementarea comună a Python (și nu trebuie confundată cu Cython numit similar)? Sau vreau să spun cu totul altceva? Poate că mă refer oblic la Jython, sau IronPython, sau PyPy. Sau poate chiar am plecat de la capăt și vorbesc despre RPython sau RubyPython (care sunt lucruri foarte, foarte diferite).
În timp ce tehnologiile menționate mai sus sunt denumite și menționate în mod obișnuit, unele dintre ele servesc unor scopuri complet diferite (sau, cel puțin, funcționează în moduri complet diferite).
De-a lungul timpului în care am lucrat cu interfețele Python, am întâlnit tone de aceste instrumente .*ython. Dar până de curând mi-am făcut timp să înțeleg ce sunt, cum funcționează și de ce sunt necesare (în felul lor).
În acest tutorial, voi începe de la zero și voi trece prin diferitele implementări Python, încheind cu o introducere detaliată în PyPy, care cred că este viitorul limbajului.
Totul începe cu o înțelegere a ceea ce este de fapt „Python”.
Dacă înțelegeți bine codul mașinii, mașinile virtuale și altele asemenea, nu ezitați să treceți mai departe.
„Este Python interpretat sau compilat?”
Acesta este un punct comun de confuzie pentru începătorii Python.
Primul lucru de realizat atunci când faceți o comparație este că „Python” este o interfață . Există o specificație a ceea ce ar trebui să facă Python și cum ar trebui să se comporte (ca și în cazul oricărei interfețe). Și există mai multe implementări (ca orice interfață).
Al doilea lucru de realizat este că „interpretat” și „compilat” sunt proprietăți ale unei implementări , nu o interfață .
Deci întrebarea în sine nu este chiar bine formulată.
Acestea fiind spuse, pentru cea mai obișnuită implementare Python (CPython: scris în C, adesea denumit pur și simplu „Python”, și cu siguranță ce folosiți dacă nu aveți idee despre ce vorbesc), răspunsul este: interpretat , cu ceva compilatie. CPython compilează * codul sursă Python în bytecode, apoi interpretează acest bytecode, executându-l pe măsură ce merge.
* Notă: aceasta nu este o „compilare” în sensul tradițional al cuvântului. În mod obișnuit, am spune că „compilarea” înseamnă luarea unui limbaj de nivel înalt și convertirea acestuia în cod de mașină. Dar este un fel de „compilație”.
Să ne uităm la acest răspuns mai atent, deoarece ne va ajuta să înțelegem unele dintre conceptele care apar mai târziu în postare.
Cod octet vs. Cod mașină
Este foarte important să înțelegeți diferența dintre bytecode și codul mașinii (alias cod nativ), poate cel mai bine ilustrat prin exemplu:
- C se compilează în codul mașinii, care este apoi rulat direct pe procesorul dumneavoastră. Fiecare instrucțiune indică procesorului tău să mute lucrurile.
- Java se compilează în bytecode, care este apoi rulat pe Java Virtual Machine (JVM), o abstractizare a unui computer care execută programe. Fiecare instrucțiune este apoi gestionată de JVM, care interacționează cu computerul dvs.
În termeni foarte scurti: codul de mașină este mult mai rapid, dar bytecode este mai portabil și mai sigur .
Codul de mașină arată diferit în funcție de mașina dvs., dar codul de octeți arată același pe toate mașinile. S-ar putea spune că codul mașinii este optimizat pentru configurația dvs.
Revenind la implementarea CPython, procesul lanțului de instrumente este următorul:
- CPython compilează codul sursă Python în bytecode.
- Acest bytecode este apoi executat pe mașina virtuală CPython.
VM alternative: Jython, IronPython și altele
După cum am menționat mai devreme, Python are mai multe implementări. Din nou, așa cum am menționat mai devreme, cel mai comun este CPython, dar există și altele care ar trebui menționate de dragul acestui ghid de comparație. Aceasta este o implementare Python scrisă în C și considerată implementarea „implicit”.
Dar cum rămâne cu implementările alternative Python? Una dintre cele mai proeminente este Jython, o implementare Python scrisă în Java care utilizează JVM. În timp ce CPython produce bytecode pentru a rula pe CPython VM, Jython produce bytecode Java pentru a rula pe JVM (acestea sunt aceleași lucruri care sunt produse atunci când compilați un program Java).
„De ce ați folosi vreodată o implementare alternativă?”, v-ați putea întreba. Ei bine, pentru unul, aceste implementări diferite Python joacă frumos cu diferite stive de tehnologie .
CPython face foarte ușor să scrieți extensii C pentru codul dvs. Python, deoarece în cele din urmă este executat de un interpret C. Jython, pe de altă parte, face foarte ușor să lucrezi cu alte programe Java: poți importa orice clase Java fără efort suplimentar, convocând și utilizând clasele Java din programele tale Jython. (Deoparte: dacă nu te-ai gândit îndeaproape, este de fapt o nebunie. Suntem în punctul în care poți amesteca și amesteca diferite limbi și le poți compila pe toate până la aceeași substanță. (După cum a menționat Rostin, programele care amestecul Fortran și codul C există de ceva vreme. Deci, desigur, acest lucru nu este neapărat nou. Dar este totuși cool.))
De exemplu, acesta este codul Jython valid:
[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 este o altă implementare populară Python, scrisă în întregime în C# și care vizează stiva .NET. În special, rulează pe ceea ce ați putea numi mașina virtuală .NET, Common Language Runtime (CLR) de la Microsoft, comparabilă cu JVM.
Ai putea spune că Jython : Java :: IronPython : C# . Acestea rulează pe aceleași VM respective, puteți importa clase C# din codul dvs. IronPython și clase Java din codul dvs. Jython etc.
Este absolut posibil să supraviețuiești fără a atinge vreodată o implementare Python non-CPython. Dar există avantaje care pot fi obținute din schimbare, dintre care majoritatea depind de stiva dvs. de tehnologie. Folosind o mulțime de limbaje bazate pe JVM? Jython ar putea fi pentru tine. Totul despre stiva .NET? Poate ar trebui să încercați IronPython (și poate că ați făcut-o deja).
Apropo: deși acesta nu ar fi un motiv pentru a folosi o implementare diferită, rețineți că aceste implementări diferă de fapt în ceea ce privește comportamentul dincolo de modul în care tratează codul dvs. sursă Python. Cu toate acestea, aceste diferențe sunt de obicei minore și se dizolvă sau apar în timp, deoarece aceste implementări sunt în curs de dezvoltare activă. De exemplu, IronPython utilizează implicit șiruri Unicode; CPython, totuși, este implicit la ASCII pentru versiunile 2.x (eșec cu UnicodeEncodeError pentru caracterele non-ASCII), dar acceptă șiruri Unicode în mod implicit pentru 3.x.
Compilare just-in-time: PyPy și viitorul
Deci avem o implementare Python scrisă în C, una în Java și una în C#. Următorul pas logic: o implementare Python scrisă în... Python. (Cititorul educat va observa că acest lucru este ușor înșelător.)
Aici lucrurile ar putea deveni confuze. Mai întâi, să discutăm despre compilarea just-in-time (JIT).
JIT: De ce și cum
Amintiți-vă că codul nativ de mașină este mult mai rapid decât bytecode. Ei bine, ce se întâmplă dacă am putea compila o parte din bytecode-ul nostru și apoi să-l rulăm ca cod nativ? Ar trebui să plătim un anumit preț pentru a compila bytecode (adică, timpul), dar dacă rezultatul final ar fi mai rapid, ar fi grozav! Aceasta este motivația compilației JIT, o tehnică hibridă care combină beneficiile interpreților și ale compilatorilor. În termeni de bază, JIT dorește să utilizeze compilarea pentru a accelera un sistem interpretat.
De exemplu, o abordare comună adoptată de JIT:

- Identificați bytecode care este executat frecvent.
- Compilați-l până la codul mașină nativ.
- Memorați rezultatul în cache.
- Ori de câte ori același bytecode este setat să fie rulat, luați în schimb codul de mașină pre-compilat și culegeți beneficiile (adică creșteri de viteză).
Iată despre ce se referă implementarea PyPy: aducerea JIT în Python (vezi Anexa pentru eforturile anterioare). Există, desigur, și alte obiective: PyPy își propune să fie multiplatformă, ușoară în memorie și fără stive. Dar JIT este într-adevăr punctul său de vânzare. Ca medie pe o grămadă de teste de timp, se spune că îmbunătățește performanța cu un factor de 6,27. Pentru o defalcare, consultați acest grafic din Centrul de viteză PyPy:
PyPy este greu de înțeles
PyPy are un potențial uriaș și, în acest moment, este foarte compatibil cu CPython (deci poate rula Flask, Django etc.).
Dar există multă confuzie în jurul PyPy (vezi, de exemplu, această propunere fără sens de a crea un PyPyPy...). În opinia mea, asta se datorează în primul rând pentru că PyPy este de fapt două lucruri:
Un interpret Python scris în RPython (nu Python (am mințit înainte)). RPython este un subset de Python cu tastare statică. În Python, este „în cea mai mare parte imposibil” să raționezi riguros despre tipuri (De ce este atât de greu? Ei bine, luați în considerare faptul că:
x = random.choice([1, "foo"])
ar fi cod Python valid (credit lui Ademan). Care este tipul de
x
? Cum putem raționa despre tipurile de variabile atunci când tipurile nici măcar nu sunt strict aplicate?). Cu RPython, sacrifici o oarecare flexibilitate, dar în schimb faci mult, mult mai ușor să raționezi despre gestionarea memoriei și altele, ceea ce permite optimizări.Un compilator care compilează cod RPython pentru diverse ținte și adaugă în JIT. Platforma implicită este C, adică un compilator RPython-to-C, dar puteți viza și JVM-ul și altele.
Doar pentru claritate în acest ghid de comparație Python, mă voi referi la acestea ca PyPy (1) și PyPy (2).
De ce ai avea nevoie de aceste două lucruri și de ce sub același acoperiș? Gândiți-vă la asta astfel: PyPy (1) este un interpret scris în RPython. Deci, preia codul Python al utilizatorului și îl compilează până la bytecode. Dar interpretul însuși (scris în RPython) trebuie interpretat de o altă implementare Python pentru a rula, nu?
Ei bine, am putea folosi doar CPython pentru a rula interpretul. Dar asta nu ar fi foarte rapid.
În schimb, ideea este că folosim PyPy (2) (denumit RPython Toolchain) pentru a compila interpretul PyPy până la cod pentru o altă platformă (de exemplu, C, JVM sau CLI) pentru a rula pe mașina noastră, adăugând JIT ca bine. Este magic: PyPy adaugă dinamic JIT la un interpret, generând propriul compilator! ( Din nou, aceasta este o nebunie: compilam un interpret, adăugând un alt compilator separat, de sine stătător. )
În cele din urmă, rezultatul este un executabil independent care interpretează codul sursă Python și exploatează optimizările JIT. Ceea ce ne-am dorit! Este o gură, dar poate această diagramă vă va ajuta:
Pentru a reitera, adevărata frumusețe a lui PyPy este că am putea scrie noi înșine o grămadă de interpreți Python diferiți în RPython fără să ne facem griji pentru JIT. PyPy ar implementa apoi JIT pentru noi folosind RPython Toolchain/PyPy (2).
De fapt, dacă devenim și mai abstracti, teoretic ai putea scrie un interpret pentru orice limbă, ai putea să-l alimentezi în PyPy și să obții un JIT pentru acea limbă. Acest lucru se datorează faptului că PyPy se concentrează pe optimizarea interpretului real, mai degrabă decât pe detaliile limbajului pe care îl interpretează.
Ca o scurtă digresiune, aș dori să menționez că JIT-ul în sine este absolut fascinant. Utilizează o tehnică numită urmărire, care se execută după cum urmează:
- Rulați interpretul și interpretați totul (adăugând fără JIT).
- Faceți niște profiluri ușoare ale codului interpretat.
- Identificați operațiunile pe care le-ați efectuat anterior.
- Compilați acești biți de cod până la codul mașinii.
Pentru mai multe, această lucrare este foarte accesibilă și foarte interesantă.
Pentru a finaliza: folosim compilatorul RPython-to-C al lui PyPy (sau altă platformă țintă) pentru a compila interpretul implementat de RPython de la PyPy.
Încheierea
După o comparație lungă a implementărilor Python, trebuie să mă întreb: De ce este atât de grozav? De ce merită urmărită această idee nebună? Cred că Alex Gaynor a spus bine pe blogul său: „[PyPy este viitorul] pentru că [el] oferă o viteză mai bună, mai multă flexibilitate și este o platformă mai bună pentru creșterea lui Python.”
În scurt:
- Este rapid pentru că compilează codul sursă în cod nativ (folosind JIT).
- Este flexibil, deoarece adaugă JIT la interpretul dvs. cu foarte puțină muncă suplimentară.
- Este flexibil (din nou) pentru că vă puteți scrie interpreții în RPython , care este mai ușor de extins decât, de exemplu, C (de fapt, este atât de ușor încât există un tutorial pentru a vă scrie propriile interpreți).
Anexă: Alte nume Python pe care este posibil să le fi auzit
Python 3000 (Py3k): o denumire alternativă pentru Python 3.0, o versiune Python majoră, incompatibilă cu înapoi, care a apărut în 2008. Echipa Py3k a prezis că va dura aproximativ cinci ani pentru ca această nouă versiune să fie pe deplin adoptată. Și în timp ce majoritatea (avertisment: afirmație anecdotică) dezvoltatorilor Python continuă să folosească Python 2.x, oamenii sunt din ce în ce mai conștienți de Py3k.
- Cython: un superset de Python care include legături pentru a apela funcții C.
- Scop: vă permite să scrieți extensii C pentru codul dvs. Python.
- De asemenea, vă permite să adăugați tastare statică la codul dvs. Python existent, permițându-i să fie compilat și să atingeți performanțe asemănătoare C.
- Acesta este similar cu PyPy, dar nu la fel. În acest caz, impuneți introducerea codului utilizatorului înainte de a-l transmite unui compilator. Cu PyPy, scrieți un Python vechi simplu, iar compilatorul se ocupă de orice optimizări.
Numba: un „compilator specializat just-in-time” care adaugă JIT la codul Python adnotat . În cei mai simpli termeni, îi dai câteva indicii și accelerează porțiuni din codul tău. Numba vine ca parte a distribuției Anaconda, un set de pachete pentru analiza și managementul datelor.
IPython: foarte diferit de orice altceva discutat. Un mediu de calcul pentru Python. Interactiv cu suport pentru seturile de instrumente GUI și experiența browserului etc.
- Psyco: un modul de extensie Python și unul dintre primele eforturi Python JIT. Cu toate acestea, de atunci a fost marcat ca „neîntreținut și mort”. De fapt, dezvoltatorul principal al Psyco, Armin Rigo, lucrează acum pe PyPy.
Legături de limbaj Python
RubyPython: o punte între mașinile virtuale Ruby și Python. Vă permite să încorporați codul Python în codul Ruby. Definiți unde pornește și se oprește Python, iar RubyPython distribuie datele între mașinile virtuale.
PyObjc: legături de limbaj între Python și Objective-C, acționând ca o punte între ele. Practic, asta înseamnă că puteți utiliza bibliotecile Objective-C (inclusiv tot ceea ce aveți nevoie pentru a crea aplicații OS X) din codul dvs. Python și modulele Python din codul dvs. Objective-C. În acest caz, este convenabil ca CPython să fie scris în C, care este un subset al Objective-C.
PyQt: în timp ce PyObjc vă oferă legături pentru componentele GUI OS X, PyQt face același lucru pentru cadrul de aplicație Qt, permițându-vă să creați interfețe grafice bogate, să accesați baze de date SQL, etc. Un alt instrument menit să aducă simplitatea lui Python în alte cadre.
Cadre JavaScript
pyjs (Pyjamas): un cadru pentru crearea de aplicații web și desktop în Python. Include un compilator Python-to-JavaScript, un set de widget-uri și alte instrumente.
Brython: o VM Python scrisă în JavaScript pentru a permite executarea codului Py3k în browser.