Course Website
Cours : Jeudi 8h15–9h00, CM 1 4
Exercices : Jeudi 9h15–11h, INF 3, BC 07-08
Cette page contient un résumé des différences principales entre Python et C++. Avec le premier cours ex cathedra, elle constitue votre principale ressource pour transférer vers Python vos connaissances du premier semestre.
Type C++ | Type Python | Valeur C++ | Valeur Python |
---|---|---|---|
int , long , size_t |
int (taille arbitraire) |
5 |
5 |
unsigned int |
N/A | ||
double (float ) |
float |
5.5 |
5.5 |
bool |
bool |
true |
True |
std::string |
str |
"hello" |
"hello" , 'hello' |
char |
N/A (str de longueur 1) |
'c' |
"c" , 'c' |
std::vector<T> (ex. : std::vector<int> ) |
list[T] (ex. : list[int] ) |
vector<int> { 2, 4 } |
[2, 4] |
void |
None |
N/A | None |
Tous ces types sont prédéfinis en Python.
Il n’y a besoin d’aucun import
, même pour utiliser str
et list
.
En Python, les int
sont toujours signés, et ont une taille arbitrairement grande.
Vous pouvez parfaitement calculer 50!
avec des int
, par exemple (essayez !).
Évidemment, plus ils deviennent grands, plus ils consomment de mémoire et prennent du temps de calcul, même pour des opérations “élémentaires”.
La plupart des programmes n’utiliseront que des “petits” entiers (par exemple tenant sur 64 bits).
Souvent, cette distinction n’est donc pas très importante.
Notez l’usage des crochets []
, au lieu des chevrons <>
, pour spécifier le type d’éléments d’une list
.
En C++, void
n’a aucune valeur.
En Python, il existe une vraie valeur de type None
, qui s’écrit aussi None
.
C++ | Python |
---|---|
Commentaire de ligne | |
|
|
Commentaire multi-lignes | |
|
N/A |
C++ utilise la syntaxe
type_de_retour nom_fonction(type_param1 param1, ...) {
// corps
}
En Python, la syntaxe est
def nom_fonction(param1: type_param1, ...) -> type_de_retour:
# corps
C++ | Python |
---|---|
Quelques exemples | |
|
|
|
|
|
|
Le corps d’une fonction est indenté de 4 espaces. C’est l’indentation qui détermine où s’arrête le corps, comme pour tous les autres blocs.
La chaîne entre """
s’appelle la docstring d’une fonction.
C’est dans cette chaîne que l’on place la documentation/spécification.
Elle s’affiche dans VS Code lorsqu’on passe le curseur sur le nom d’une fonction.
Il n’est pas possible de déclarer plusieurs fonctions avec le même nom en Python. Il n’y a pas d’overloading, comme en C++.
De plus, il n’y a pas de notion de prototype de fonction en Python. On peut utiliser des fonctions définies plus loin dans le fichier sans déclaration explicite.
On peut se demander comment écrire un bloc vide, si les blocs sont régis entièrement par l’indentation.
La réponse est : on ne peut pas !
C’est pourquoi Python possède une instruction pass
, qui ne fait rien :
def do_nothing() -> None:
pass
C++ | Python |
---|---|
|
|
Python n’utilise pas de ;
à la fin des instructions.
Chaque nouvelle ligne débute une nouvelle instruction.
En C++, il est habituel de spécifier explicitement le type des variables, surtout pour des types simples.
On n’utilise auto
que dans relativement peu de situations.
En Python, c’est l’inverse.
Pour les variables locales, on n’utilisera généralement pas de type explicite.
On laisse leurs types être inférés.
On n’utilise un type explicite que pour attirer l’attention sur celui-ci.
Notez que dans VS Code, si vous passez votre souris sur une variable, une bulle d’aide vous montrera son type (inféré ou non).
C++ | Python |
---|---|
Affichage à l'écran | |
|
|
|
|
|
|
Arithmétique | |
|
|
|
|
|
|
Opérateurs booléens | |
|
|
|
|
|
|
Appels de fonctions | |
|
|
En Python, on peut explicitement écrire le nom d'un paramètre lors de l'appel. C'est le plus souvent utilisé pour des paramètres qui ont une valeur par défaut. | |
N/A |
|
Opérations sur les vecteurs/listes et chaînes de caractères ("collections" en général) | |
|
|
|
|
Le | |
|
|
|
|
Opérations de modification des vecteurs/listes | |
|
|
|
|
|
|
Tout comme le corps des fonctions, les structures de contrôles introduisent leur corps avec :
.
Le corps doit être indenté (de 4 espaces par convention), et se termine par indentation.
C++ | Python |
---|---|
Branchement conditionnel | |
|
|
|
|
|
|
Remarquez le mot-clef | |
Itération sur une collection | |
|
|
Itération par indices | |
De | |
|
|
De | |
|
|
De | |
|
|
Les itérations ci-dessus sont valides avec | |
Boucles de type "Tant que" | |
|
|
Python n'a pas de boucle de type "Répéter ... Tant que".
Si vraiment nécessaire, vous pouvez obtenir le même effet avec une boucle | |
|
|
Branchement multiple | |
|
|
L'instruction |
Au premier semestre, en C++, vous n’avez étudié que les struct
s comme structures de données personnalisées.
Il existe aussi des class
es, mais ce n’est pas au programme du premier semestre.
Python ne possède que des class
es.
Cependant, une forme particulière de classe en Python correspond à la notion de struct
de C++.
Il s’agit des data classes (classes de données).
Une structure telle que
struct Point {
int x;
int y;
}
correspond en Python à la dataclass suivante :
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
C++ | Python |
---|---|
Création d'une instance | |
|
|
Lecture et modification des champs | |
|
|
Affichage d'un point complet (typiquement pour déboguer) | |
N/A |
|
En C++, on peut spécifier individuellement les champs qui ne peuvent pas être modifiés. En Python, il faut décider pour la dataclass en entier si elle est muable ou pas.
La version immuable de Point
s’écrirait en C++ comme
struct Point {
const int x;
const int y;
}
et en Python avec une dataclass frozen :
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
Dès la deuxième semaine du cours, nous introduirons la notion plus générale de classe, qui est au coeur de la méthodologie orientée objet.
C++ | Python |
---|---|
Définition d'un alias de type | |
|
|
Attention, ici, ça pique ! C++ et Python sont en désaccord fondamental sur les notions de valeurs, références, et pointeurs. Au delà de toutes leurs différences de surface que nous avons listées jusqu’ici, c’est sans doute ici ce qui fait leurs réelles spécificités. Nous en avons déjà parlé lors du tout premier cours, mais je tiens quand même à exposer ici ces concepts.
En C++, on manipule les données par valeur. Lorsque l’on assigne une variable à une autre, c’est la valeur qui est transférée. Concrètement, cela veut dire qu’on effectue toujours une copie. Par exemple :
Point p { 5, 7 };
Point q = p; // copie de la valeur
Une conséquence directe est que, lorsque l’on modifie p
, le point q
n’en est pas affecté.
p.x = 11;
cout << q.x << endl; // toujours 5
Avec un système par valeurs, la “boîte” nommée par une variable contient directement une valeur.
Il est possible de modifier le contenu de cette boîte (comme avec p.x = 11
), et même de remplacer complètement la boîte (p = Point { 13, 15 }
).
Dans un cas comme dans l’autre, toute autre copie de cette valeur reste inchangée.
Si nous souhaitons que p
et q
restent “connectées”, nous pouvons utilisers des pointeurs à la place.
Dans ce cas, les variables p
et q
sont toujours des boîtes, mais les valeurs dans ces boîtes ne sont que des pointeurs vers d’autre boîtes.
Copier q = p
revient donc à copier la valeur pointeur, et non pas la valeur pointée.
shared_ptr<Point> p = make_shared<Point>(Point { 5, 7 });
shared_ptr<Point> q = p; // copie de la *valeur pointeur*
Remarquez que la boîte de type Point
n’a pas de nom !
Ni p
, ni q
.
Les boîtes qui portent un nom sont des valeurs pointeur.
Dans cette situation, si l’on modifie le champ x
de la boîte pointée par p
, naturellement, cette modification est reflétée sur q
.
En effet, c’est la même boîte !
p->x = 11;
cout << q->x << endl; // 11
Dans ce modèle, il y a une question très subtile : que se passe-t-il lorsqu’on remplace la boîte dans son entièreté ? Est-ce que les deux pointeurs sont toujours connectés ? En C++, il y a en fait moyen d’obtenir chacun de ces comportements, en fonction de l’instruction exacte qu’on utilise !
Si on utilise
*p = Point { 30, 20 };
cout << q->x << endl; // 30
alors on suit d’abord la flèche du pointeur (l’opérateur *
), puis on écrase le contenu de la boîte.
On se retrouve alors avec la situation suivante, dans laquelle p
et q
sont en effet toujours connectés.
Par contre, si l’on écrit
p = make_shared<Point>(Point { 15, 17 });
cout << q->x << endl; // toujours 30
alors c’est la valeur pointeur que l’on écrase, et l’on met une nouvelle flèche à la place.
On écrase la boîte nommée p
, et non pas la boîte pointée.
On est alors dans une toute nouvelle situation, où p
n’est plus connecté à q
.
&
de C++Les références de C++, déclarées avec &
, sont un modèle assez unique qui donne plusieurs noms à la même boîte.
Encore une fois, notez bien la différence avec les pointeurs : les boîtes nommées p
et q
sont bien des boîtes différentes quand ce sont des pointeurs.
Elles contiennent toutes les deux des flèches pointant vers la même (troisième) boîte, mais ce sont des boîtes différentes.
Avec les références de C++, si on les définit comme
Point p { 5, 7 };
Point& q = p; // création d'un alias par référence
obtient une situation telle que celle-ci :
Dans cette situation, même p = Point { 15, 17 }
ne peut pas déconnecter p
de q
.
En effet, cette fois, ces deux noms correspondent exactement à la même boîte.
Il ne faudra pas confondre ce modèle de “références C++” avec le modèle par références de Python. Les références C++ sont assez uniques en leur genre. Le modèle par références de Python est plus répandu : on le retrouve dans de nombreux autres langages, tels que Java, Scala, JavaScript, etc.
Le modèle de Python utilise un seul concept : la référence à un objet. Dans ce modèle, toutes les variables (et champs, éléments de listes, etc.) contiennent une flèche. Cette flèche pointe sur un objet, ou instance d’une classe (on utilise “objet” ou “instance” de manière interchangeable).
Lorsqu’on effectue une assignation, c’est donc bien la flèche qui est copiée, comme avec le modèle de pointeurs de C++. Toutes les autres opérations, notamment les accès aux champs, suivent la flèche pour travailler sur l’objet.
Rappel : nous travaillons avec la dataclass Point
(sa version muable), définie comme
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
Avec les initialisations suivantes :
p = Point(5, 7)
q = p
on obtient la situation que l’on avait avec les pointeurs en C++ :
La boîte intitulée Point
est une instance de la classe Point
.
Les deux variables p
et q
contiennent deux flèches qui pointent vers cette même instance.
Si l’on modifie un champ de p
, nous modifions en fait l’instance pointée par p
.
Donc, q
est affectée :
p.x = 11
print(q.x) # 11
Anecdote : si vous jouez à des MMORPGs, vous avez sans doute l’habitude d’entrer dans une instance d’un donjon. Il s’agit exactement du même concept ! Vous partagez cette instance avec votre groupe, c’est-à-dire que toutes et tous les membres du groupe ont une flèche vers la même instance du donjon. D’autres groupes qui attaquent le même donjon que vous (la même classe de donjon), au même moment, font référence à d’autres instances. C’est ainsi que votre groupe peut vaincre ensemble les monstres du donjon, sans pour autant que les autres groupes n’en soient affectés.
En revanche, si l’on réassigne p
avec une nouvelle instance de Point
, nous changeons la flèche de p
, qui est donc déconnecté de q
.
p = Point(15, 17)
print(q.x) # toujours 11
On peut donc considérer que toutes les variables Python sont des pointeurs. On appelle cela des références.
Remarquez que Python n’a aucun équivalent de l’instruction C++
*p = Point(30, 20);
C’était la variante qui suivait d’abord la flèche, puis écrasait la boîte. Cette variante n’existe pas en Python. Soit on remplace la flèche elle-même, soit on accède aux champs de l’instance. On ne peut jamais écraser la boîte d’une instance elle-même.
Si vous lisez d’autres sources de documentation sur Python ou d’autres langages, il est probable que vous y trouviez des explications du type :
Les primitives sont passées par valeur, et les objets sont passés par référence.
Cette distinction est, à mon avis, une complication inutile.
En fait, même les primitives sont des objets ! Si l’on définit
x = 5
on le représente graphiquement comme ceci :
Puisque les objets int
s sont immuables, même si plusieurs variables ont une référence vers le même objet int
, on ne pourra jamais le détecter.
Aucun code ne peut modifier le contenu de l’instance, et donc ne peut affecter d’autres parties du code !
C’est donc un atout majeur d’avoir des instances immuables (frozen=True
pour les dataclasses).
Sauf bonnes raisons, il faudrait toujours définir nos classes comme immuables.
Mais… si même les int
s sont des références… n’avons-nous pas menti sur nos diagrammes précédents avec les Point
s ?
Eh oui !
Si l’on veut être exact, on devrait représenter les Point
s comme ceci :
Pour des valeurs immuables simples (et notamment les int
, float
, bool
et str
), nous ferons souvent la simplification graphique que nous avons faite plus haut.
En effet, puisque personne ne peut voir la différence, c’est acceptable.
C++ | Python |
---|---|
Lancer une exception | |
|
|
Contrairement à C++, où l'on peut lancer "n'importe quoi" comme exception, en Python les exceptions doivent être des instances des types prévus à cet effet.
Python possède un certain nombre de types d'exceptions prédéfinis.
Consultez la documentation pour plus d'information.
| |
Attraper une exception | |
|
|
Il peut y avoir plusieurs blocks | |
Relancer une exception | |
|
|
Effectuer des actions de "nettoyage" | |
Parfois, nous avons besoin d'effectuer des actions de "nettoyage" (cleanup) après un bloc de code.
Et ce, que le bloc de code se soit bien terminé, ou qu'il ait déclenché une exception.
C'est peu utile en C++ pour des raisons qui dépassent le cadre de ces explications, mais c'est courant dans le autres langages.
Les instructions | |
|
|
En C++, on utilise des modificateurs sur les flots pour formatter des données.
En Python, comme dans beaucoup d’autres langages, les fonctions d’entrées/sorties ne gèrent pas elles-mêmes le formattage.
À la place, on peut construire des chaînes formattées, et on envoie ensuite des chaînes formattées sur les sorties.
Cela veut dire qu’on peut aussi utiliser les mêmes mécaniques pour obtenir des str
formattées, sans nécessairement les envoyer sur un flot.
C++ | Python |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Et on peut bien sûr tout combiner... | |
|
|
C++ | Python |
---|---|
Écriture dans un fichier | |
|
|
L'argument L'argument | |
Lecture depuis un fichier | |
|
|
Lecture depuis l'entrée standard | |
|
|
C++ utilise des méchanismes silencieux pour les erreurs.
Il faut explicitement tester f.fail()
pour gérer ces erreurs.
En Python, toutes les erreurs de manipulations de fichiers, ou de lecture de nombres, déclenchent des exceptions. Il s’agit donc de traiter adéquatement les exceptions, s’il y a lieu. Par exemple, ici on tente de lire deux entiers depuis un fichier, et on vérifie qu’il n’y a pas d’erreur.
try:
with open("file.txt", "r", encoding="utf-8", newline='') as f:
x = int(f.readline())
y = int(f.readline())
except OSError as e:
print("Le fichier n'a pas pu être lu :")
print(e)
except ValueError as e:
print("Une des lignes ne contenait pas un entier en base 10 :")
print(e)