2016-11-09

Compilé, Interprété et POO ? C++ ou Python ?

Cette article a été écrit à l'origine pour introduire la programmation à des débutants voulant choisir par quoi commencer entre le C++ et le Python. Il contient donc des simplifications et ne fait que gratter la surface des différentes questions abordées.

Dans cet article, on va essayer de s'attaquer un petit peu à ce qui diffère entre le Python et le C++. Une grande partie de ce que vous pourrez lire ici est suffisamment générique pour vous servir même en dehors de ces deux langages dans une certaine mesure.

En bref…

Le Python est un langage interprété, le C++ un langage compilé et ils sont tous les deux orientés objets.

Compilé, interprété et orienté objet ?

Il faut sûrement revenir sur ce qu'est un langage interprété, un langage compilé et un langage orienté objets (très souvent abrégé POO ou OOP en anglais).

Pour faire tourner un programme développé dans un langage compilé, il est nécessaire de passer par une étape qu'on appelle la compilation. C'est ce qu'on appelle un compilateur qui s'en occupera. De façon générale, un compilateur va s'occuper de transformer un code source écrit dans un langage de programmation précis vers un autre langage exploitable par la machine cible. Dans le cas du C++, ce sera donc vers le langage assembleur qui est lui-même une traduction directe du langage machine (la traduction de l'assembleur vers le langage machine est appelé « l'assemblage », c'est une opération extrêmement simple à effectuer puisqu'une instruction d'assembleur est équivalent à une instruction précise du processeur). L'avantage, c'est que le processeur lit le langage machine sans aucun effort. Le soucis, c'est qu'il y a différents langages machine (cela dépend des processeurs). Mais ce n'est pas tellement notre problème, puisque ce qui nous intéresse c'est le C++, et la traduction, c'est le travail du compilateur.

Pour faire tourner un programme développé dans un langage interprété, il est cette fois nécessaire de passer par ce qui s'appelle un interpréteur. L'interpréteur est un programme généralement disponible en langage machine (parce qu'il faut bien que le processeur puisse comprendre à un moment donné) et qui va s'occuper de lire le code source pour, en quelque sorte, remplacer le processeur. C'est une sorte sur-couche, qui permet une certaine flexibilité, mais par contre votre programme sera automatiquement plus lent.

Il faut néanmoins un peu nuancer. Dans le cas de Python par exemple, nous ne sommes pas exactement en présence d'un langage interprété, mais plutôt un langage semi-interprété. Le code source est en fait traduit dans un langage intermédiaire (que l'on appelle bytecode dans le cas de Python) et c'est ce langage qui sera interprété. Il est en fait techniquement parfaitement possible de compiler le langage intermédiaire en langage assembleur / machine (pour ceux qui connaissent, en C, le compilateur gcc génère des fichiers .o qui sont en fait des fichiers de « code objet », un langage intermédiaire avant la traduction en assembleur). Il existe d'ailleurs des moyens de compiler du code Python (cf Cython), mais pour avoir essayé, je peux vous dire que c'est pas tellement commode.

Maintenant, qu'est-ce qu'un langage orienté objet ? Tout d'abord, qu'est-ce qu'un programme ? Pour simplifier, nous allons dire que :

Programme = Données + Algorithmes

Ce modèle nous permet de considérer deux choix dans la conception d'un programme :

Le premier cas a l'avantage d'être intuitif et facile à appliquer sur des problèmes de petite taille. Le soucis, c'est que le code est souvent assez difficile à ré-utiliser, on a souvent besoin d'adapter ses données pour utiliser du code externe (librairies, …), le code est plus difficile à faire évoluer dans le temps. Or on dit souvent que l'évolution d'un logiciel représente 70% de son cycle de vie, et la majorité des modifications concernent plutôt les données que les algorithmes. D'où cette difficulté de gérer l'évolution d'un programme en utilisant uniquement une méthodologie ultra centrée sur les algorithmes.

C'est là qu'intervient la programmation orientée objet, qui met davantage l'emphase sur les données. On défini les données et ensuite on développe les algorithmes pour manipuler ces données. L'association entre les données et les algorithmes adaptés mène à ce qu'on appelle des « Objets ». On peut visualiser un objet comme étant une boîte contenant des données et des boutons qui permettent d'effectuer des traitements (cette image n'est d'ailleurs pas un hasard, comme on aura l'occasion de le voir en particulier avec le C++).

Schéma d'un objet

Il reste encore beaucoup à dire sur la programmation orientée objet comme par exemple ce qu'on considère comme les quatre piliers qui sont « l'abstraction », « l'encapsulation », « l'héritage » et le « polymorphisme ». Sachez toutefois que ces « piliers » ne sont en réalité pas considérés comme des piliers par tout le monde. Il existe aussi des langages qui ne contiennent pas certaines de ces caractéristiques (comme par exemple le language Rust qui ne propose pas de mécanisme d'héritage), mais permettent tout de même d'assurer l'évolution et la ré-utilisation du code. Il n'existe rien qui puisse répondre à tous les problèmes, et un même problème peut être résolu de plusieurs façons différentes (vous savez sûrement déjà que c'est vrai dans plein de situations).

Revenons-en au C++ et au Python

De façon générale, un programme en Python tourne plus lentement qu'un programme C++ (les différences peuvent être énormes selon ce que vous faites). Maintenant que vous connaissez la différence entre un langage compilé et un langage interprété, vous vous doutez sûrement en partie de la raison. Cependant, le fait de passer par un interpréteur n'est pas la seule raison pour laquelle Python est plus lent. En fait, Python est également plus lent que Java (qui fonctionne pourtant à peu près sur le même modèle, compilation en langage intermédiaire, le bytecode java dont l'extension de fichier est .class). C'est notamment dû au typage de Python qui est différent du typage du Java et du C++.

Je vais simplifier la notion de « typage » pour le moment. Dans la plupart des langages de programmation, on trouve ce qui s'appelle des variables (en fait je n'en connaît aucun qui n'en a pas). Du point de vue du programmeur, une variable est une sorte d'étiquette qui correspond à un certain emplacement dans la mémoire de l'ordinateur contenant une certaine quantité de données.

Voici comment l'on va déclarer des variables en C++ :

int anInteger;
anInteger = 5;
// peut aussi s'écrire en une seule ligne :
int anotherInteger = 5;

Ici, int est le type de la variable (abréviation de integer qui signifie « entier ») et anInteger son nom. À la deuxième ligne, on affecte la valeur 5 qui est de type int à la variable anInteger qui est aussi de type int.

En Python :

anInteger = 5

Donc on remarque déjà qu'en Python, même pas besoin de déclarer la variable, ni même de préciser son type. Mais cela va encore plus loin.

Voici un autre exemple. En C++ :

int anInteger = 5;
anInteger = "Hello!"; // aïe, aïe, ça ne va pas compiler !

Voilà ce qui se passe : à la première ligne, on déclare que la variable anInteger est de type int. Et à la deuxième ligne, on veut lui affecter une valeur qui est une chaîne de caractères (du texte) (note pour ceux qui ont déjà fait du C : le type est alors char*). Comme les deux types ne sont pas identiques, le compilateur va râler. Il paraît logique qu'un entier ne puisse pas être du texte, non ?

En Python :

anInteger = 5
anInteger = "Hello!" # Aucun problème.

En Python, une variable peut parfaitement changer de type à la volée. C'est pour cela que l'on dit que le typage du Python est « dynamique ». Maintenant la variable « anInteger » est une chaîne de caractères.

Nous allons aussi jeter un rapide coup d'œil à la façon dont on déclare une fonction dans les deux langages. Je ne vous demande pas de tout comprendre immédiatement, je veux juste vous montrer à quel point le C++ permet d'être minutieux et le Python permissif.

En C++ :

void printHelloWorld(void) { /* code */ }
int max(int a, int b) { /* code */ }
Purée moulinex(const Patate& patate) { /* code */ }

En Python :

def printHelloWorld():
    # code
def max(a, b):
    # code
def moulinex(patate):
    # code

Il est important de proposer de la documentation aussi bien en C++ qu'en Python, mais en Python il arrive que sans documentation on ne puisse tout simplement pas comprendre quel genre d'arguments une fonction attend sans regarder le code (je passe aussi sur le fait qu'une fonction en Python peut renvoyer des données de plusieurs types différents, contrairement au C++ où une fonction ne peut que renvoyer une donnée correspondant strictement au type indiqué).

La première remarque que l'on peut faire à ce stade, c'est qu'à terme, on peut facilement se retrouver à avoir des problèmes de consistance si l'on n'a pas été extrêmement rigoureux : des variables qui n'ont pas le type attendu au bon moment par exemple. Un programme peut devenir complexe et prendre de la taille et il faut bien veiller à ne pas faire n'importe quoi, car l'interpréteur ne vous dira pas que votre code a un problème jusqu'à ce que ça plante pendant l'exécution du programme (on peut même imaginer des situations où votre programme ne plante pas systématiquement, bonne chance pour trouver le problème dans ce cas). Voilà pourquoi je pense qu'il est plus compliqué de gérer un gros projet en Python (néanmoins pas impossible, ce genre de projets existent). Pour des programmes de taille modeste, en général cela ne pose aucun problème.

L'autre remarque, c'est qu'au niveau de l'interpréteur, du côté de Python, lorsque par exemple on va se retrouver à effectuer des opérations comme tout simplement a + b, il va déjà falloir vérifier que a et b peuvent être additionnés. En C++, si ça compile, c'est qu'à priori, on peut bien faire cette addition. La vérification est effectuée au moment de la compilation en se basant sur les types des variables et le code machine s'exécutera sans avoir à re-effectuer cette vérification.

Par contre, il est vrai que l'on peut se retrouver à écrire facilement 5 fois plus de code en C++ pour obtenir un programme équivalent et qu'il est beaucoup plus simple d'écrire du code Python rapidement.

En fait, Python brille plutôt dans un rôle de « super glue », de contrôleur « haut niveau », qui s'occupe de relier divers composants « bas niveau » (comprenez « proche de la machine ») écrits par exemple en C++. Je dirais même qu'utiliser un langage haut niveau et un langage bas niveau de cette façon donne une excellente combinaison. Il existe de nombreuses librairies écrites en C++ pouvant être utilisées en Python. Et c'est notamment toutes ces librairies qui font la force de Python. On va pouvoir faire du calcul scientifique en utilisant la bonne librairie écrite en C++ qui fait les dits calculs plus rapidement. On va pouvoir faire des jeux vidéo en utilisant une libraire qui s'occupera de gérer le matériel pour nous, ou qui nous fournira un moteur physique (par contre, dès que vous allez vouloir faire votre propre moteur physique en Python, il vaudra mieux que cela reste modeste…).

L'autre avantage de Python, c'est qu'il est certainement plus rapide à apprendre que le C++. Mais je trouve que l'on apprend davantage de choses en C++ (choses qui servent toujours même en Python bien que l'on ne s'en rende pas compte).

On pourrait encore continuer longtemps à analyser les différences entre les deux langages, mais je pense que l'on a vu les différences principales permettant de faire un choix éclairé.