TD9. Classes, instances et références
Le sujet de ce TD est disponible au format pdf ici.
L'objectif de ce TD est d'enfoncer le clou à propos des concepts de classe, d'instance et de référence introduit dans le dernier CM.
Préambule : éléments de langage
Question 1
Donner une définition du terme classe.
Question 2
Donner une définition du terme instance.
Question 3
Donner une définition du terme référence.
Question 4
Donner une définition du terme variable ainsi qu'une ligne de code, dans n'importe quel langage, permettant de créer une variable de type entier.
Correction globale
Cliquez ici pour révéler la correction.
- Classe : définition d'un nouveau type composé d'un ensemble d'attributs. Contient aussi des opérations sur le type en question, appelées méthodes, mais en BPI ça ne nous intéresse pas car on ne fait pas de programmation objet, on verra ça en 2A dans le cours POO. Bon, ça nous intéresse quand même un peu, parce que quand on fait
nombres.append("23")
c'est bien la méthodeappend
de la classelist
que nous appelons alors que quand nous faisonssum(nombres)
nous appelons la fonctionsum
. - Instance : zone mémoire contenant un ensemble d'attributs tels que définis par la classe à laquelle l'instance est attachée. Cette classe définit le type de l'instance.
- Référence : zone mémoire qui est simplement un lien vers une instance.
- Variable : nom symbolique utilisé pour désigner une zone mémoire de la machine. En Python, les variables, les paramètres et les attributs sont toujours des références.
Une introduction aux concepts de classe, instance, référence et variable est également disponible en vidéo :
Exercice 1 : en Python, toutes les variables sont des références !
Et nous allons le prouver !
Question 1
Nous savons maintenant que les variables Python sont toujours des références vers des instances. Comme nous l'avons fait dans le premier cours (diapositive 21 ici) et les premiers TD, exécutons pas à pas le programme suivant en modifiant sur papier un tableau contenant les variables accessibles à chaque étape du programme ainsi que les instances référencées par ces variables. Ce tableau indiquera pour chaque variable : son nom, son type, sa portée et sa valeur. La valeur sera donc dessinée par une flèche vers l'instance associée.
Autrement dit, les schémas que nous allons faire dans ce TD allient les tableaux de variables vus en début d'année avec les schémas d'instances tels que dessinés par le module traceur que nous utiliserons en TP.
1 2 3 4 5 6 7 8 |
|
Correction question 1
Cliquez ici pour révéler la correction.
Voici les quatre schémas corrects représentant l'état du programme :
après exécution de la ligne 5 :
après exécution de la ligne 6 :
après exécution de la ligne 7 :
après exécution de la ligne 8 :
Les points essentiels à retenir de cette question sont les suivants :
- à partir de maintenant, nous allons insister sur les dessins des instances qui se trouvent dans une partie de la mémoire appelée le tas car ils aident vraiment à comprendre nos programmes ;
- nous afficherons les variables en vert ;
- nous afficherons les instances en gris avec leur type et leur contenu ;
- nous représenterons les références par des flèches.
- comme toutes les variables sont des références, leur valeur est donc simplement une adresse vers une instance dans le tas ;
- il existe bien une classe
int
fournie par l'interpréteur ; - sur le deuxième schéma (et sur le dernier), on voit que
j
(etk
) désigne(nt) la même instanceint 42
quei
;i
etj
(etk
à la fin) sont des références comme toutes les variables en Python.
Question 2
Que se passe-t-il si l'on rajoute une ligne k = 17
?
Correction question 2
Cliquez ici pour révéler la correction.
Après k = 17
, voici l'état de la mémoire :
Les points essentiels à retenir de cette question sont les suivants :
- comme les variables en Python sont toujours des références, quand on modifie
k
aveck = 17
on ne change pasi
etj
mais on affecte une nouvelle valeur à la référencek
pour que celle-ci pointe vers une nouvelle instance deint
contenant la valeur17
. - comme les entiers sont immuables, il n'y a aucun problème à ce que l'entier
42
soit "partagé" par les variables référençant cet entier et l'interpréteur fait ce choix de partage pour des questions d'optimisation.
Question 3
Dessiner l'état du programme ci-dessous, c'est à dire le tableau des variables et les instances en mémoire, après exécution de:
- la première ligne ;
- des deux premières lignes ;
- des quatre lignes.
1 2 3 4 5 6 7 8 |
|
Correction question 3
Cliquez ici pour révéler la correction.
Voici les trois schémas corrects représentant l'état du programme :
après exécution de la ligne 5 :
après exécution de la ligne 6 :
après exécution de la ligne 8 :
Les points essentiels à retenir de cette question sont les suivants :
- il existe bien une classe
list
fournie par l'interpréteur ; - les
list
Python contiennent des références ; - les instances de la classe
list
sont mutables, attention donc si l'on a plusieurs références sur une même instance delist
, lorsque l'on modifie cette instance via une référence, on "modifie" donc implicitement toutes les références ; - ici il y a deux opérations de mutation sur la liste, à savoir la modification de la référence
[1]
et la modification de la référence[2]
.
Correction vidéo pour l'ensemble de l'exercice. Attention cette correction ne représente que les instances dans le tas (à l'aide du traceur) et pas le tableau des variables.
Exercice 2 : et donc les paramètres sont aussi des références !
Nous avons vu que les paramètres des fonctions sont des variables locales aux fonctions. Comme toutes les variables Python sont des références, les paramètres le sont aussi et c'est ce que nous allons voir dans cet exercice.
Question 1
Dessiner l'état du programme ci-dessous, c'est à dire le tableau des variables et les instances en mémoire :
- après exécution de la ligne 12 ;
- à l'entrée de la fonction
add_1
, c'est à dire juste avant l'exécution de la ligne 7; - après exécution de la ligne 7 ;
- après exécution de la ligne 13.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Correction question 1
Cliquez ici pour révéler la correction.
Voici les quatre schémas corrects représentant l'état du programme :
après exécution de la ligne 12 :
avant exécution de la ligne 7 :
après exécution de la ligne 7 :
après exécution de la ligne 13
Les points essentiels à retenir de cette question sont les suivants :
- les paramètres et les valeurs de retour des fonctions sont des références ;
- quand on affecte une nouvelle valeur à un paramètre à l'intérieur d'une fonction (avec
param = ...
), on fait pointer le paramètre qui est une variable locale à la fonction vers une autre instance ; - à la ligne 13, la variable
entier
de la fonctionmain
est modifiée : sa nouvelle valeur est la référence renvoyée par la fonctionadd_1
, donc une flèche vers l'entier7
; - si une instance n'est plus référencée par personne, alors le ramasse miette de l'interpréteur peut decider de la supprimer pour libérer la mémoire qu'elle occupe.
- (pythonisterie : les
f-string
c'est cool, voir la documentation ici et/ou essayer le code en TP pour comprendre ce que faitf"{entier = }"
)
Question 2
Dessiner l'état du programme ci-dessous, c'est à dire le tableau des variables et les instances en mémoire :
- après exécution de la ligne 11 ;
- à l'entrée de la fonction
add_1
, c'est à dire juste avant l'exécution de la ligne 7; - après exécution de la ligne 7 mais avant de sortir de la fonction
add_1
; - après exécution de la ligne 12.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Correction question 2
Cliquez ici pour révéler la correction.
Voici les quatre schémas corrects représentant l'état du programme :
après exécution de la ligne 11 :
avant exécution de la ligne 7 :
après exécution de la ligne 7 mais avant de sortir de la fonction add_1
:
après exécution de la ligne 12
Les points essentiels à retenir de cette question sont les suivants :
- les paramètres et les valeurs de retour des fonctions sont des références ;
- quand on modifie une instance mutable à l'intérieur d'une fonction, les modifications sont visibles à l'extérieur de la fonction si on possède une référence vers l'instance qui a été modifiée. C'est le cas sur le tableau dynamique
entiers
ici.
Exercice 3 : première classe
Nous allons maintenant voir comment définir nos propres classes. On considère le programme suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Question 1
Écrire une classe Point
composée de deux attributs x
et y
et fournissant :
- un constructeur prenant une abscisse et une ordonnée en paramètres ;
- un opérateur de conversion en chaîne de caractères
__str__
.
Question 2
Réécrire le code à l'aide de la classe Point
Correction question 2
Cliquez ici pour révéler la correction.
Voici la classe Point
et la nouvelle version de la fonction manipule_points()
utilisant cette classe.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Les points essentiels à retenir de cette question sont les suivants :
- la méthode spéciale
__init__()
d'une classe est appelée automatiquement par l'interpréteur quand on crée une instance de cette classe ; - la méthode spéciale
__str__()
d'une classe est appelée automatiquement par l'interpréteur quand on faitstr(inst)
et queinst
est une référence vers une instance de la classe ; - les instances de nos propres classes sont mutables ;
- la fonction
print
appelle__str__()
pour nous sur tous ses paramètres qui ne sont pas des références vers des instances de typestr
.
Question 3
Dessiner l'état du programme ci-dessous après exécution de la ligne milieu.y = 5
.
Comme nous nous intéressons ici non pas à l'évolution des variables et des instances au cours du temps, mais simplement à comprendre ce qu'il se passe à un instant donné, nous ne dessinerons pas le tableau des variables mais directement les variables et instances en mémoire comme les dessinerait le module traceur
.
Correction question 3
Cliquez ici pour révéler la correction.
Voici le dessin correct représentant l'état du programme :
Les points essentiels à retenir de cette question sont les suivants :
- il y a bien trois points, c'est à dire des instances de la classe
Point
, correspondant aux trois appelsPoint(..., ...)
; - les attributs de nos propres classes sont des références ;
- l'entier
5
est partagé entre deux points ; - l'abscisse de milieu est un
float
.
Correction vidéo pour l'ensemble de l'exercice :
Exercice 4 : deuxième classe
Question 1
Proposer une classe Triangle
utilisant un tableau contenant trois références vers des instances de la classe Point
.
Penser à définir l'opérateur __str__
.
Correction question 1
Cliquez ici pour révéler la correction.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Question 2
Dessiner l'état du programme ci-dessous, après exécution des quatre lignes.
Comme nous nous intéressons ici non pas à l'évolution des variables et des instances au cours du temps, mais simplement à comprendre ce qu'il se passe à un instant donné, nous ne dessinerons pas le tableau des variables mais directement les variables et instances en mémoire comme les dessinerait le module traceur
.
1 2 3 4 |
|
Correction question 2
Cliquez ici pour révéler la correction.
Voici le dessin correct représentant l'état du programme :
Les points essentiels à retenir de cette question sont les suivants :
p1
ett1.points[0]
sont deux références vers la même instance ;p2
ett1.points[1]
sont deux références vers la même instance ;p3
ett1.points[2]
sont deux références vers la même instance ;- il y a une instance de
list
, créée quand on fait[...]
.
Question 3
Dessiner l'état du programme ci-dessous juste après le dernier appel à la fonction print
du programme ci-dessous.
Comme nous nous intéressons ici non pas à l'évolution des variables et des instances au cours du temps, mais simplement à comprendre ce qu'il se passe à un instant donné, nous ne dessinerons pas le tableau des variables mais directement les variables et instances en mémoire comme les dessinerait le module traceur
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Correction question 3
Cliquez ici pour révéler la correction.
Voici le dessin correct représentant l'état du programme :
Les points essentiels à retenir de cette question sont les suivants :
- la méthode
__str__
est appelée automatiquement par l'interpréteur sutt1
ett2
quand on faitprint(t1, t2)
; - une instance de
list
est créée à chaque[...]
, donc deux instances sont créées ici ; - les
list
Python contiennent des références et donct1
ett2
"partagent" un point qui est une instance ⚠️mutable⚠. Donc quand on change l'instance dePoint
partagée, on change les deux triangles.
Correction vidéo pour l'ensemble de l'exercice :
Exercice 5 : égaux ou identiques ? (pour aller plus loin)
Question 1
Qu'affiche le programme ci-dessous ?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
Correction question 1
Cliquez ici pour révéler la correction.
Il faut commencer par copier le code ci-dessus dans un fichier Python et l'exécuter.
On peut également regarder ce qu'il se passe en mémoire :
Voici ce que l'on peut retenir de cet exercice :
x is y
teste si les deux référencesx
ety
pointent vers la même instance ;x == y
teste si les deux instances référencées parx
ety
sont égales, c'est à dire si leur contenu sont égaux, par exemple pour des entiers si ils ont la même valeur. Plus précisément,x == y
est défini parx.__eq__(y)
voir ici- pour les instances immuables, comment fait l'interpréteur pour décider si il doit chercher une instance existante ayant la même valeur ou en créer un nouveau, ben c'est une bonne question :-) ? Tout ce qu'on peut dire ici c'est que notre entier favori
42
est partagé, alors que7006652
ne l'est pas.
Question 2
Qu'affiche le programme ci-dessous ?
1 2 3 4 5 6 7 8 9 10 |
|
Correction question 2
Cliquez ici pour révéler la correction.
Le programme affiche deux fois False
.
Nous avons deux instances différentes de la classe Point
, donc il est normal que le teste avec is
renvoie False
.
Concernant le teste avec ==
sur des instances de nos propres classes, par défaut celui-ci est équivalent à is
. C'est pourquoi il renvoie False
également.
Pour avoir un test d'égalité qui compare la valeur de deux Point
, c'est à dire les valeurs des abscisses et celles des ordonnées, il faut implémenter la méthode spéciale __eq__
dans notre classe Point
voir ici.