Perché ci sono così tanti pitoni? Un confronto di implementazione Python
Pubblicato: 2022-03-11Python è fantastico.
Sorprendentemente, questa è un'affermazione abbastanza ambigua. Cosa intendo con 'Python'? Intendo Python l' interfaccia astratta? Intendo CPython, l' implementazione Python comune (e da non confondere con Cython dal nome simile)? O intendo qualcosa di completamente diverso? Forse mi riferisco obliquamente a Jython, o IronPython, o PyPy. O forse sono davvero andato fuori di testa e sto parlando di RPython o RubyPython (che sono cose molto, molto diverse).
Sebbene le tecnologie sopra menzionate siano comunemente denominate e comunemente referenziate, alcune di esse servono a scopi completamente diversi (o, almeno, operano in modi completamente diversi).
Durante il mio tempo lavorando con le interfacce Python, mi sono imbattuto in tonnellate di questi strumenti .*ython. Ma non fino a poco tempo fa mi sono preso il tempo per capire cosa sono, come funzionano e perché sono necessari (a modo loro).
In questo tutorial, inizierò da zero e mi sposterò attraverso le varie implementazioni di Python, concludendo con un'introduzione approfondita a PyPy, che credo sia il futuro del linguaggio.
Tutto inizia con la comprensione di cosa sia effettivamente "Python".
Se hai una buona conoscenza del codice macchina, delle macchine virtuali e simili, sentiti libero di andare avanti.
"Python è interpretato o compilato?"
Questo è un punto di confusione comune per i principianti di Python.
La prima cosa da capire quando si effettua un confronto è che 'Python' è un'interfaccia . C'è una specifica di cosa dovrebbe fare Python e come dovrebbe comportarsi (come con qualsiasi interfaccia). E ci sono più implementazioni (come con qualsiasi interfaccia).
La seconda cosa da capire è che "interpretato" e "compilato" sono proprietà di un'implementazione , non un'interfaccia .
Quindi la domanda in sé non è molto ben formulata.
Detto questo, per l'implementazione Python più comune (CPython: scritto in C, spesso indicato semplicemente come 'Python', e sicuramente quello che stai usando se non hai idea di cosa sto parlando), la risposta è: interpretato , con qualche compilation. CPython compila * il codice sorgente Python in bytecode, quindi interpreta questo bytecode, eseguendolo come va.
* Nota: questa non è 'compilation' nel senso tradizionale della parola. In genere, diremmo che "compilazione" prende un linguaggio di alto livello e lo converte in codice macchina. Ma è una sorta di "compilazione".
Diamo un'occhiata a quella risposta più da vicino, poiché ci aiuterà a capire alcuni dei concetti che verranno fuori più avanti nel post.
Bytecode e codice macchina
È molto importante capire la differenza tra bytecode e codice macchina (noto anche come codice nativo), forse meglio illustrato dall'esempio:
- C viene compilato in codice macchina, che viene quindi eseguito direttamente sul tuo processore. Ogni istruzione indica alla tua CPU di spostare le cose.
- Java viene compilato in bytecode, che viene quindi eseguito sulla Java Virtual Machine (JVM), un'astrazione di un computer che esegue programmi. Ogni istruzione viene quindi gestita dalla JVM, che interagisce con il tuo computer.
In parole povere: il codice macchina è molto più veloce, ma il bytecode è più portatile e sicuro .
Il codice macchina ha un aspetto diverso a seconda della macchina, ma il bytecode ha lo stesso aspetto su tutte le macchine. Si potrebbe dire che il codice macchina è ottimizzato per la tua configurazione.
Tornando all'implementazione di CPython, il processo della toolchain è il seguente:
- CPython compila il tuo codice sorgente Python in bytecode.
- Quel bytecode viene quindi eseguito sulla macchina virtuale CPython.
VM alternative: Jython, IronPython e altro
Come accennato in precedenza, Python ha diverse implementazioni. Anche in questo caso, come accennato in precedenza, il più comune è CPython, ma ce ne sono altri che dovrebbero essere menzionati per il bene di questa guida comparativa. Questa è un'implementazione Python scritta in C e considerata l'implementazione "predefinita".
Ma per quanto riguarda le implementazioni Python alternative? Uno dei più importanti è Jython, un'implementazione Python scritta in Java che utilizza JVM. Mentre CPython produce bytecode da eseguire su CPython VM, Jython produce Java bytecode da eseguire su JVM (questa è la stessa roba che viene prodotta quando si compila un programma Java).
"Perché mai dovresti usare un'implementazione alternativa?", potresti chiedere. Bene, per esempio, queste diverse implementazioni di Python funzionano bene con diversi stack tecnologici .
CPython rende molto facile scrivere estensioni C per il tuo codice Python perché alla fine viene eseguito da un interprete C. Jython, d'altra parte, rende molto facile lavorare con altri programmi Java: puoi importare qualsiasi classe Java senza alcuno sforzo aggiuntivo, richiamando e utilizzando le tue classi Java dall'interno dei tuoi programmi Jython. (A parte: se non ci hai pensato da vicino, in realtà è pazzesco. Siamo al punto in cui puoi mescolare e mischiare linguaggi diversi e compilarli tutti nella stessa sostanza. (Come accennato da Rostin, programmi che mix Fortran e codice C sono in circolazione da un po '. Quindi, ovviamente, questo non è necessariamente nuovo. Ma è comunque bello.))
Ad esempio, questo è un codice Jython valido:
[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 è un'altra popolare implementazione di Python, scritta interamente in C# e destinata allo stack .NET. In particolare, funziona su quella che potresti chiamare .NET Virtual Machine, il Common Language Runtime (CLR) di Microsoft, paragonabile alla JVM.
Si potrebbe dire che Jython : Java :: IronPython : C# . Vengono eseguiti sulle stesse rispettive VM, puoi importare classi C# dal tuo codice IronPython e classi Java dal tuo codice Jython, ecc.
È totalmente possibile sopravvivere senza mai toccare un'implementazione Python non CPython. Ma ci sono vantaggi dal passaggio, la maggior parte dei quali dipendono dal tuo stack tecnologico. Utilizzi molti linguaggi basati su JVM? Jython potrebbe fare al caso tuo. Tutto sullo stack .NET? Forse dovresti provare IronPython (e forse lo hai già fatto).
A proposito: anche se questo non sarebbe un motivo per utilizzare un'implementazione diversa, nota che queste implementazioni in realtà differiscono nel comportamento oltre al modo in cui trattano il tuo codice sorgente Python. Tuttavia, queste differenze sono in genere minori e si dissolvono o emergono nel tempo poiché queste implementazioni sono in fase di sviluppo attivo. Ad esempio, IronPython utilizza le stringhe Unicode per impostazione predefinita; CPython, tuttavia, per impostazione predefinita è ASCII per le versioni 2.x (non avendo un UnicodeEncodeError per i caratteri non ASCII), ma supporta le stringhe Unicode per impostazione predefinita per 3.x.
Compilazione just-in-time: PyPy e il futuro
Quindi abbiamo un'implementazione Python scritta in C, una in Java e una in C#. Il prossimo passo logico: un'implementazione Python scritta in... Python. (Il lettore istruito noterà che questo è leggermente fuorviante.)
Ecco dove le cose potrebbero confondersi. Innanzitutto, discutiamo della compilazione just-in-time (JIT).
JIT: Il perché e il come
Ricordiamo che il codice macchina nativo è molto più veloce del bytecode. E se potessimo compilare parte del nostro bytecode e quindi eseguirlo come codice nativo? Dovremmo pagare un prezzo per compilare il bytecode (cioè il tempo), ma se il risultato finale fosse più veloce, sarebbe fantastico! Questa è la motivazione della compilazione JIT, una tecnica ibrida che unisce i vantaggi di interpreti e compilatori. In termini di base, JIT vuole utilizzare la compilazione per velocizzare un sistema interpretato.

Ad esempio, un approccio comune adottato dalle SIC:
- Identifica il bytecode che viene eseguito frequentemente.
- Compilalo nel codice macchina nativo.
- Memorizza il risultato nella cache.
- Ogni volta che lo stesso bytecode è impostato per essere eseguito, prendi invece il codice macchina precompilato e cogli i benefici (ad esempio, aumenti di velocità).
Questo è l'obiettivo dell'implementazione di PyPy: portare JIT in Python (vedi l' Appendice per gli sforzi precedenti). Ci sono, ovviamente, altri obiettivi: PyPy mira a essere multipiattaforma, leggero per la memoria e di supporto senza stack. Ma JIT è davvero il suo punto di forza. Come media su una serie di test temporali, si dice che migliori le prestazioni di un fattore di 6,27. Per una ripartizione, vedere questo grafico dal PyPy Speed Center:
PyPy è difficile da capire
PyPy ha un potenziale enorme e, a questo punto, è altamente compatibile con CPython (quindi può eseguire Flask, Django, ecc.).
Ma c'è molta confusione intorno a PyPy (vedi, ad esempio, questa proposta senza senso per creare un PyPyPy...). Secondo me, è principalmente perché PyPy è in realtà due cose:
Un interprete Python scritto in RPython (non Python (ho mentito prima)). RPython è un sottoinsieme di Python con tipizzazione statica. In Python, è "per lo più impossibile" ragionare rigorosamente sui tipi (perché è così difficile? Considera il fatto che:
x = random.choice([1, "foo"])
sarebbe un codice Python valido (credito ad Ademan). Qual è il tipo di
x
? Come possiamo ragionare sui tipi di variabili quando i tipi non sono nemmeno rigorosamente applicati?). Con RPython, sacrifichi una certa flessibilità, ma rendi invece molto, molto più facile ragionare sulla gestione della memoria e quant'altro, il che consente ottimizzazioni.Un compilatore che compila il codice RPython per vari target e aggiunge in JIT. La piattaforma predefinita è C, ovvero un compilatore da RPython a C, ma potresti anche indirizzare la JVM e altri.
Solo per chiarezza in questa guida al confronto di Python, mi riferirò a questi come PyPy (1) e PyPy (2).
Perché dovresti aver bisogno di queste due cose e perché sotto lo stesso tetto? Pensala in questo modo: PyPy (1) è un interprete scritto in RPython. Quindi prende il codice Python dell'utente e lo compila in bytecode. Ma l'interprete stesso (scritto in RPython) deve essere interpretato da un'altra implementazione Python per funzionare, giusto?
Bene, potremmo semplicemente usare CPython per eseguire l'interprete. Ma non sarebbe molto veloce.
Invece, l'idea è che usiamo PyPy (2) (denominato RPython Toolchain) per compilare l'interprete di PyPy fino al codice per un'altra piattaforma (ad esempio, C, JVM o CLI) da eseguire sulla nostra macchina, aggiungendo JIT come bene. È magico: PyPy aggiunge dinamicamente JIT a un interprete, generando il proprio compilatore! ( Ancora una volta, questo è pazzo: stiamo compilando un interprete, aggiungendo un altro compilatore separato e autonomo. )
Alla fine, il risultato è un eseguibile autonomo che interpreta il codice sorgente Python e sfrutta le ottimizzazioni JIT. Che è proprio quello che volevamo! È un boccone, ma forse questo diagramma aiuterà:
Per ribadire, la vera bellezza di PyPy è che potremmo scrivere noi stessi un gruppo di diversi interpreti Python in RPython senza preoccuparci di JIT. PyPy implementerebbe quindi JIT per noi utilizzando RPython Toolchain/PyPy (2).
In effetti, se diventiamo ancora più astratti, potresti teoricamente scrivere un interprete per qualsiasi lingua, inviarlo a PyPy e ottenere un JIT per quella lingua. Questo perché PyPy si concentra sull'ottimizzazione dell'interprete effettivo, piuttosto che sui dettagli della lingua che sta interpretando.
Come breve digressione, vorrei ricordare che la stessa JIT è assolutamente affascinante. Utilizza una tecnica chiamata traccia, che viene eseguita come segue:
- Esegui l'interprete e interpreta tutto (non aggiungendo JIT).
- Esegui una leggera profilazione del codice interpretato.
- Identifica le operazioni che hai eseguito in precedenza.
- Compila questi bit di codice fino a codice macchina.
Per di più, questo documento è altamente accessibile e molto interessante.
Per concludere: utilizziamo il compilatore RPython-to-C (o altra piattaforma di destinazione) di PyPy per compilare l'interprete implementato da RPython di PyPy.
Avvolgendo
Dopo un lungo confronto tra le implementazioni di Python, devo chiedermi: perché è così eccezionale? Perché vale la pena perseguire questa pazza idea? Penso che Alex Gaynor lo abbia messo bene sul suo blog: "[PyPy è il futuro] perché [offre] migliore velocità, maggiore flessibilità ed è una piattaforma migliore per la crescita di Python".
In breve:
- È veloce perché compila il codice sorgente in codice nativo (usando JIT).
- È flessibile perché aggiunge la JIT al tuo interprete con pochissimo lavoro aggiuntivo.
- È flessibile (di nuovo) perché puoi scrivere i tuoi interpreti in RPython , che è più facile da estendere rispetto, diciamo, a C (in effetti, è così facile che c'è un tutorial per scrivere i tuoi interpreti).
Appendice: altri nomi Python che potresti aver sentito
Python 3000 (Py3k): un nome alternativo per Python 3.0, una versione principale di Python non compatibile con le versioni precedenti che è entrata in scena nel 2008. Il team di Py3k ha previsto che ci sarebbero voluti circa cinque anni per l'adozione completa di questa nuova versione. E mentre la maggior parte degli sviluppatori Python (attenzione: affermazione aneddotica) continuano a utilizzare Python 2.x, le persone sono sempre più consapevoli di Py3k.
- Cython: un superset di Python che include collegamenti per chiamare le funzioni C.
- Obiettivo: permetterti di scrivere estensioni C per il tuo codice Python.
- Consente inoltre di aggiungere la digitazione statica al codice Python esistente, consentendo la compilazione e il raggiungimento di prestazioni simili a C.
- Questo è simile a PyPy, ma non è lo stesso. In questo caso, stai forzando la digitazione del codice dell'utente prima di passarlo a un compilatore. Con PyPy, scrivi un semplice vecchio Python e il compilatore gestisce qualsiasi ottimizzazione.
Numba: un "compilatore specializzato just-in-time" che aggiunge JIT al codice Python annotato . Nei termini più elementari, gli dai alcuni suggerimenti e velocizza parti del tuo codice. Numba fa parte della distribuzione Anaconda, un insieme di pacchetti per l'analisi e la gestione dei dati.
IPython: molto diverso da qualsiasi altra cosa discussa. Un ambiente informatico per Python. Interattivo con supporto per toolkit GUI ed esperienza browser, ecc.
- Psyco: un modulo di estensione Python e uno dei primi sforzi di Python JIT. Tuttavia, da allora è stato contrassegnato come "non mantenuto e morto". In effetti, lo sviluppatore principale di Psyco, Armin Rigo, ora lavora su PyPy.
Collegamenti in linguaggio Python
RubyPython: un ponte tra le VM Ruby e Python. Ti permette di incorporare il codice Python nel tuo codice Ruby. Definisci dove si avvia e si arresta Python e RubyPython esegue il marshalling dei dati tra le macchine virtuali.
PyObjc: collegamenti linguistici tra Python e Objective-C, fungendo da ponte tra di loro. In pratica, ciò significa che puoi utilizzare le librerie Objective-C (incluso tutto il necessario per creare applicazioni OS X) dal tuo codice Python e i moduli Python dal tuo codice Objective-C. In questo caso, è conveniente che CPython sia scritto in C, che è un sottoinsieme di Objective-C.
PyQt: mentre PyObjc ti offre il binding per i componenti della GUI di OS X, PyQt fa lo stesso per il framework dell'applicazione Qt, permettendoti di creare ricche interfacce grafiche, accedere a database SQL, ecc. Un altro strumento mirato a portare la semplicità di Python in altri framework.
Framework JavaScript
pyjs (Pyjamas): un framework per la creazione di applicazioni web e desktop in Python. Include un compilatore Python-to-JavaScript, un set di widget e alcuni altri strumenti.
Brython: una VM Python scritta in JavaScript per consentire l'esecuzione del codice Py3k nel browser.