Pourquoi y a-t-il tant de Pythons ? Une comparaison d'implémentation Python

Publié: 2022-03-11

Python est incroyable.

Étonnamment, c'est une déclaration assez ambiguë. Qu'est-ce que je veux dire par "Python" ? Est-ce que je veux dire Python l' interface abstraite ? Est-ce que je veux dire CPython, l' implémentation Python commune (et à ne pas confondre avec le Cython du même nom) ? Ou est-ce que je veux dire autre chose entièrement? Peut-être que je fais référence obliquement à Jython, ou IronPython, ou PyPy. Ou peut-être que je suis vraiment allé au fond des choses et que je parle de RPython ou de RubyPython (qui sont des choses très, très différentes).

Bien que les technologies mentionnées ci-dessus soient communément nommées et communément référencées, certaines d'entre elles ont des objectifs complètement différents (ou, du moins, fonctionnent de manière complètement différente).

Tout au long de mon travail avec les interfaces Python, j'ai rencontré des tonnes de ces outils .*ython. Mais ce n'est que récemment que j'ai pris le temps de comprendre ce qu'ils sont, comment ils fonctionnent et pourquoi ils sont nécessaires (à leur manière).

Dans ce didacticiel, je vais partir de zéro et parcourir les différentes implémentations de Python, en terminant par une introduction approfondie à PyPy, qui, à mon avis, est l'avenir du langage.

Tout commence par une compréhension de ce qu'est réellement "Python".

Si vous avez une bonne compréhension du code machine, des machines virtuelles, etc., n'hésitez pas à passer à autre chose.

« Python est-il interprété ou compilé ? »

C'est un point de confusion courant pour les débutants en Python.

La première chose à réaliser lors d'une comparaison est que 'Python' est une interface . Il y a une spécification de ce que Python doit faire et comment il doit se comporter (comme avec n'importe quelle interface). Et il existe plusieurs implémentations (comme avec n'importe quelle interface).

La deuxième chose à réaliser est que 'interprété' et 'compilé' sont des propriétés d'une implémentation , pas d'une interface .

La question elle-même n'est donc pas vraiment bien formée.

Python est-il interprété ou compilé ? La question n'est pas vraiment bien formée.

Cela dit, pour l'implémentation Python la plus courante (CPython : écrit en C, souvent appelé simplement "Python", et sûrement ce que vous utilisez si vous n'avez aucune idée de quoi je parle), la réponse est : interprété , avec quelques compilations. CPython compile * le code source Python en bytecode, puis interprète ce bytecode, en l'exécutant au fur et à mesure.

* Remarque : il ne s'agit pas de "compilation" au sens traditionnel du terme. En règle générale, nous dirions que la "compilation" consiste à prendre un langage de haut niveau et à le convertir en code machine. Mais c'est une sorte de "compilation".

Examinons cette réponse de plus près, car elle nous aidera à comprendre certains des concepts qui seront abordés plus tard dans le post.

Bytecode contre code machine

Il est très important de comprendre la différence entre le bytecode et le code machine (c'est-à-dire le code natif), peut-être mieux illustré par un exemple :

  • C se compile en code machine, qui est ensuite exécuté directement sur votre processeur. Chaque instruction demande à votre processeur de déplacer des éléments.
  • Java se compile en bytecode, qui est ensuite exécuté sur la machine virtuelle Java (JVM), une abstraction d'un ordinateur qui exécute des programmes. Chaque instruction est ensuite gérée par la JVM, qui interagit avec votre ordinateur.

En termes très brefs : le code machine est beaucoup plus rapide, mais le bytecode est plus portable et sécurisé .

Le code machine est différent selon votre machine, mais le bytecode est le même sur toutes les machines. On pourrait dire que le code machine est optimisé pour votre configuration.

Pour en revenir à l'implémentation de CPython, le processus de la chaîne d'outils est le suivant :

  1. CPython compile votre code source Python en bytecode.
  2. Ce bytecode est ensuite exécuté sur la machine virtuelle CPython.
Les débutants supposent souvent que Python est compilé à cause des fichiers .pyc. Il y a du vrai là-dedans : le fichier .pyc est le bytecode compilé, qui est ensuite interprété. Donc, si vous avez déjà exécuté votre code Python et que vous avez le fichier .pyc à portée de main, il s'exécutera plus rapidement la deuxième fois, car il n'est pas nécessaire de recompiler le bytecode.

Machines virtuelles alternatives : Jython, IronPython, etc.

Comme je l'ai mentionné plus tôt, Python a plusieurs implémentations. Encore une fois, comme mentionné précédemment, le plus courant est CPython, mais il y en a d'autres qui doivent être mentionnés pour les besoins de ce guide de comparaison. Il s'agit d'une implémentation Python écrite en C et considérée comme l'implémentation "par défaut".

Mais qu'en est-il des implémentations alternatives de Python ? L'un des plus importants est Jython, une implémentation Python écrite en Java qui utilise la JVM. Alors que CPython produit un bytecode à exécuter sur la machine virtuelle CPython, Jython produit un bytecode Java à exécuter sur la JVM (c'est la même chose qui est produite lorsque vous compilez un programme Java).

L'utilisation du bytecode Java par Jython est illustrée dans ce diagramme d'implémentation Python.

"Pourquoi utiliseriez-vous une implémentation alternative ?", pourriez-vous demander. Eh bien, d'une part, ces différentes implémentations Python fonctionnent bien avec différentes piles technologiques .

CPython facilite l'écriture d'extensions C pour votre code Python car il est finalement exécuté par un interpréteur C. Jython, d'autre part, facilite le travail avec d'autres programmes Java : vous pouvez importer n'importe quelle classe Java sans effort supplémentaire, en invoquant et en utilisant vos classes Java à partir de vos programmes Jython. (En aparté : si vous n'y avez pas réfléchi attentivement, c'est en fait fou. Nous sommes au point où vous pouvez mélanger et écraser différentes langues et les compiler toutes dans la même substance. (Comme mentionné par Rostin, les programmes qui mélanger le code Fortran et C existe depuis un moment. Donc, bien sûr, ce n'est pas nécessairement nouveau. Mais c'est quand même cool.))

Par exemple, voici un code Jython valide :

 [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 est une autre implémentation Python populaire, entièrement écrite en C# et ciblant la pile .NET. En particulier, il s'exécute sur ce que vous pourriez appeler la machine virtuelle .NET, le Common Language Runtime (CLR) de Microsoft, comparable à la JVM.

Vous pourriez dire que Jython : Java :: IronPython : C# . Ils s'exécutent sur les mêmes machines virtuelles respectives, vous pouvez importer des classes C # à partir de votre code IronPython et des classes Java à partir de votre code Jython, etc.

Il est tout à fait possible de survivre sans jamais toucher à une implémentation Python non-CPython. Mais la commutation présente des avantages, dont la plupart dépendent de votre pile technologique. Vous utilisez beaucoup de langages basés sur JVM ? Jython pourrait être pour vous. Tout sur la pile .NET ? Peut-être devriez-vous essayer IronPython (et peut-être que vous l'avez déjà fait).

Ce tableau de comparaison Python montre les différences entre les implémentations Python.

Au fait : bien que ce ne soit pas une raison pour utiliser une implémentation différente, notez que ces implémentations diffèrent en fait par leur comportement au-delà de la façon dont elles traitent votre code source Python. Cependant, ces différences sont généralement mineures et se dissolvent ou émergent au fil du temps, car ces implémentations sont en cours de développement actif. Par exemple, IronPython utilise des chaînes Unicode par défaut ; CPython, cependant, utilise par défaut ASCII pour les versions 2.x (échouant avec une UnicodeEncodeError pour les caractères non ASCII), mais prend en charge les chaînes Unicode par défaut pour 3.x.

Compilation juste-à-temps : PyPy et l'avenir

Nous avons donc une implémentation Python écrite en C, une en Java et une en C#. La prochaine étape logique : une implémentation Python écrite en… Python. (Le lecteur averti notera que cela est légèrement trompeur.)

Voici où les choses pourraient devenir confuses. Tout d'abord, discutons de la compilation juste-à-temps (JIT).

JIT : le pourquoi et le comment

Rappelez-vous que le code machine natif est beaucoup plus rapide que le bytecode. Eh bien, et si nous pouvions compiler une partie de notre bytecode et l'exécuter ensuite en tant que code natif ? Nous aurions à payer un certain prix pour compiler le bytecode (c'est-à-dire du temps), mais si le résultat final était plus rapide, ce serait génial ! C'est la motivation de la compilation JIT, une technique hybride qui mélange les avantages des interpréteurs et des compilateurs. En termes simples, JIT veut utiliser la compilation pour accélérer un système interprété.

Par exemple, une approche commune adoptée par les JIT :

  1. Identifiez le bytecode qui est exécuté fréquemment.
  2. Compilez-le en code machine natif.
  3. Mettez le résultat en cache.
  4. Chaque fois que le même bytecode est configuré pour être exécuté, récupérez plutôt le code machine précompilé et récoltez les bénéfices (c'est-à-dire, des augmentations de vitesse).

C'est en quoi consiste l'implémentation de PyPy : amener JIT à Python (voir l' annexe pour les efforts précédents). Il y a, bien sûr, d'autres objectifs : PyPy vise à être multiplateforme, léger en mémoire et compatible sans pile. Mais JIT est vraiment son argument de vente. En moyenne sur un tas de tests de temps, on dit qu'il améliore les performances d'un facteur de 6,27. Pour une ventilation, consultez ce tableau du PyPy Speed ​​Center :

L'intégration de JIT à l'interface Python à l'aide de l'implémentation PyPy est payante en termes d'amélioration des performances.

PyPy est difficile à comprendre

PyPy a un énorme potentiel et, à ce stade, il est hautement compatible avec CPython (il peut donc exécuter Flask, Django, etc.).

Mais il y a beaucoup de confusion autour de PyPy (voir par exemple cette proposition absurde de créer un PyPyPy…). À mon avis, c'est principalement parce que PyPy est en fait deux choses :

  1. Un interpréteur Python écrit en RPython (pas Python (j'ai menti avant)). RPython est un sous-ensemble de Python avec typage statique. En Python, il est "presque impossible" de raisonner rigoureusement sur les types (Pourquoi est-ce si difficile ? Eh bien, considérez le fait que :

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

    serait un code Python valide (crédit à Ademan). Quel est le type de x ? Comment pouvons-nous raisonner sur les types de variables alors que les types ne sont même pas strictement appliqués ?). Avec RPython, vous sacrifiez une certaine flexibilité, mais au lieu de cela, il est beaucoup plus facile de raisonner sur la gestion de la mémoire et ainsi de suite, ce qui permet des optimisations.

  2. Un compilateur qui compile le code RPython pour diverses cibles et ajoute JIT. La plate-forme par défaut est C, c'est-à-dire un compilateur RPython vers C, mais vous pouvez également cibler la JVM et d'autres.

Uniquement pour plus de clarté dans ce guide de comparaison Python, je les appellerai PyPy (1) et PyPy (2).

Pourquoi auriez-vous besoin de ces deux choses, et pourquoi sous le même toit ? Pensez-y de cette façon : PyPy (1) est un interpréteur écrit en RPython. Il prend donc le code Python de l'utilisateur et le compile en bytecode. Mais l'interpréteur lui-même (écrit en RPython) doit être interprété par une autre implémentation Python pour fonctionner, n'est-ce pas ?

Eh bien, nous pourrions simplement utiliser CPython pour exécuter l'interpréteur. Mais ce ne serait pas très rapide.

Au lieu de cela, l'idée est que nous utilisons PyPy (2) (appelé la chaîne d'outils RPython) pour compiler l'interpréteur de PyPy en code pour une autre plate-forme (par exemple, C, JVM ou CLI) à exécuter sur notre machine, en ajoutant JIT comme bien. C'est magique : PyPy ajoute dynamiquement JIT à un interpréteur, générant son propre compilateur ! ( Encore une fois, c'est fou : nous compilons un interpréteur, en ajoutant un autre compilateur séparé et autonome. )

Au final, le résultat est un exécutable autonome qui interprète le code source Python et exploite les optimisations JIT. C'est exactement ce que nous voulions! C'est une bouchée, mais peut-être que ce schéma vous aidera:

Ce diagramme illustre la beauté de l'implémentation de PyPy, y compris un interpréteur, un compilateur et un exécutable avec JIT.

Pour réitérer, la vraie beauté de PyPy est que nous pourrions nous écrire un tas d'interpréteurs Python différents dans RPython sans nous soucier de JIT. PyPy implémenterait alors JIT pour nous en utilisant la chaîne d'outils RPython/PyPy (2).

En fait, si nous devenons encore plus abstraits, vous pourriez théoriquement écrire un interpréteur pour n'importe quelle langue, l'alimenter en PyPy et obtenir un JIT pour cette langue. En effet, PyPy se concentre sur l'optimisation de l'interpréteur réel, plutôt que sur les détails du langage qu'il interprète.

Vous pouvez théoriquement écrire un interpréteur pour n'importe quelle langue, le transmettre à PyPy et obtenir un JIT pour cette langue.

Comme une brève digression, je voudrais mentionner que le JIT lui-même est absolument fascinant. Il utilise une technique appelée traçage, qui s'exécute comme suit :

  1. Exécutez l'interpréteur et interprétez tout (sans ajouter de JIT).
  2. Effectuez un léger profilage du code interprété.
  3. Identifiez les opérations que vous avez déjà effectuées.
  4. Compilez ces morceaux de code jusqu'au code machine.

Pour en savoir plus, cet article est très accessible et très intéressant.

Pour conclure : nous utilisons le compilateur RPython-to-C (ou une autre plate-forme cible) de PyPy pour compiler l'interpréteur implémenté par RPython de PyPy.

Emballer

Après une longue comparaison des implémentations Python, je dois me demander : pourquoi est-ce si génial ? Pourquoi cette idée folle mérite-t-elle d'être poursuivie ? Je pense qu'Alex Gaynor l'a bien dit sur son blog : "[PyPy est l'avenir] car [il] offre une meilleure vitesse, plus de flexibilité et constitue une meilleure plate-forme pour la croissance de Python."

En bref:

  • C'est rapide car il compile le code source en code natif (en utilisant JIT).
  • Il est flexible car il ajoute le JIT à votre interprète avec très peu de travail supplémentaire.
  • C'est flexible (encore une fois) parce que vous pouvez écrire vos interpréteurs en RPython , ce qui est plus facile à étendre que, disons, C (en fait, c'est si facile qu'il existe un tutoriel pour écrire vos propres interpréteurs).

Annexe : autres noms Python que vous avez peut-être entendus

  • Python 3000 (Py3k) : une dénomination alternative pour Python 3.0, une version Python majeure et rétro-incompatible qui a fait son apparition en 2008. L'équipe Py3k a prédit qu'il faudrait environ cinq ans pour que cette nouvelle version soit pleinement adoptée. Et tandis que la plupart (avertissement : affirmation anecdotique) des développeurs Python continuent d'utiliser Python 2.x, les gens sont de plus en plus conscients de Py3k.

  • Cython : un sur-ensemble de Python qui inclut des liaisons pour appeler des fonctions C.
    • Objectif : vous permettre d'écrire des extensions C pour votre code Python.
    • Vous permet également d'ajouter un typage statique à votre code Python existant, lui permettant d'être compilé et d'atteindre des performances de type C.
    • Ceci est similaire à PyPy, mais pas identique. Dans ce cas, vous forcez la saisie du code de l'utilisateur avant de le transmettre à un compilateur. Avec PyPy, vous écrivez du Python ancien et le compilateur gère toutes les optimisations.

  • Numba : un "compilateur spécialisé juste-à-temps" qui ajoute JIT au code Python annoté . Dans les termes les plus élémentaires, vous lui donnez quelques conseils et cela accélère des parties de votre code. Numba fait partie de la distribution Anaconda, un ensemble de packages pour l'analyse et la gestion des données.

  • IPython : très différent de tout ce qui a été discuté. Un environnement informatique pour Python. Interactif avec prise en charge des kits d'outils GUI et de l'expérience du navigateur, etc.

  • Psyco : un module d'extension Python et l'un des premiers efforts Python JIT. Cependant, il a depuis été marqué comme "non entretenu et mort". En fait, le développeur principal de Psyco, Armin Rigo, travaille maintenant sur PyPy.

Liaisons de langage Python

  • RubyPython : un pont entre les VM Ruby et Python. Vous permet d'intégrer du code Python dans votre code Ruby. Vous définissez où Python démarre et s'arrête, et RubyPython rassemble les données entre les machines virtuelles.

  • PyObjc : liaisons de langage entre Python et Objective-C, agissant comme un pont entre eux. Concrètement, cela signifie que vous pouvez utiliser les bibliothèques Objective-C (y compris tout ce dont vous avez besoin pour créer des applications OS X) à partir de votre code Python et les modules Python à partir de votre code Objective-C. Dans ce cas, il est pratique que CPython soit écrit en C, qui est un sous-ensemble d'Objective-C.

  • PyQt : alors que PyObjc vous donne une liaison pour les composants de l'interface graphique OS X, PyQt fait de même pour le framework d'application Qt, vous permettant de créer des interfaces graphiques riches, d'accéder à des bases de données SQL, etc. Un autre outil visant à apporter la simplicité de Python à d'autres frameworks.

Cadres JavaScript

  • pyjs (Pyjamas) : un framework pour créer des applications Web et de bureau en Python. Inclut un compilateur Python vers JavaScript, un ensemble de widgets et quelques autres outils.

  • Brython : une machine virtuelle Python écrite en JavaScript pour permettre l'exécution du code Py3k dans le navigateur.