Skip to content

TD9. Classes, instances et références

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.

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.
  • 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. Une référence possède un type qui est celui de l'instance référencée.
  • Variable : nom symbolique utilisé pour désigner une zone mémoire de la machine. Une variable possède un type. En Python, toutes les variables sont 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

Dessiner l'état du programme ci-dessous, c'est à dire les variables et instances en mémoire, après exécution de chacune des 4 lignes.

1
2
3
4
5
6
7
8
#!/usr/bin/env python3

"""Un petit programme pour nous, un grand programme pour l'interpréteur"""

i = 42
j = i
k = 41
k += 1

Cliquez ici pour révéler la correction. Voici les quatre dessins corrects représentant l'état du programme :

Après ligne 1
après ligne 1

Après ligne 2
après ligne 2

Après ligne 3
après ligne 3

Après ligne 4
après ligne 4

Les points essentiels à retenir de cette question sont les suivants :

  • à partir de maintenant, nous allons insister sur les dessins mémoire car ils aident vraiment à comprendre nos programmes ;
  • les dessins ci-dessus proviennent directement de l'interpréteur Python via le module traceur ;
  • il existe bien une classe int fournie par l'interpréteur ;
  • sur le deuxième schéma, on voit que j désigne la même instance int 42 que i ;
  • i et j sont des références comme toutes les variables en Python ;
  • les variables sont affichées en vert avec leur nom par le traceur ;
  • les instances sont affichées en gris leur type et leur contenu par le traceur ;
  • les références sont affichées par des points noirs par le traceur.

Question 2

Que se passe-t-il si l'on rajoute une ligne k = 17 ?

Cliquez ici pour révéler la correction. Après k = 17, voici l'état de la mémoire :
après k = 17

Les points essentiels à retenir de cette question sont les suivants :

  • les entiers sont immutables ;
  • par conséquent, quand on modifie k avec k = 17 on ne change pas i et j mais on affecte une nouvelle valeur à la référence k pour que celle-ci pointe vers une nouvelle instance de int contenant la valeur 17.

Question 3

Dessiner l'état du programme ci-dessous, c'est à dire les variables et 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
#!/usr/bin/env python3

"""Un petit programme pour nous, un grand programme pour l'interpréteur"""

vect1 = [3, 2, 1, "go", "feu", "partez"]
vect2 = vect1
vect2[1] = 42
vect2[2] = 17

Cliquez ici pour révéler la correction. Voici les trois dessins corrects représentant l'état du programme :

Après ligne 1
après ligne 1

Après ligne 2
après ligne 2

Après ligne 3
après ligne 3

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 de list, 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 :

Exercice 2 : 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
#!/usr/bin/env python3

"""Programme manipulant des points via deux entiers"""

def manipule_points():
    """Quelques opérations sur des points"""

    x1, y1 = 3, 5
    x2, y2 = 4, 4
    milieu_x, milieu_y = (x1 + x2) / 2, (y1 + y2) / 2
    print("le milieu est (", milieu_x, milieu_y, ")")
    print("on projette sur la droite y=5")
    milieu_y = 5
    print("nous sommes maintenant en (", milieu_x, milieu_y, ")")

if __name__ == "__main__":
    manipule_points()

Question 1

Pourquoi ne pas réécrire ce programme en utilisant des namedtuple ?

Cliquez ici pour révéler la correction. Parce que les namedtuple sont immutables. On ne peut donc pas faire la projection sur la droite y=5 car elle consiste en une affectation de l'ordonnée du milieu.

Question 2

É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 chaine de caractères __str__.

Question 3

Réécrire le code à l'aide de la classe Point

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
#!/usr/bin/env python3

"""Programme manipulant des points via une classe"""


class Point:
    """Un point dans le plan"""

    def __init__(self, abscisse, ordonnee):
        self.x = abscisse
        self.y = ordonnee

    def __str__(self):
        return "(" + str(self.x) + "," + str(self.y) + ")"

def manipule_points():
    """Quelques opérations sur des points"""

    p1 = Point(3, 5)
    p2 = Point(4, 4)
    milieu = Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2)
    print("le milieu est", milieu)
    print("on projette sur la droite y=5")
    milieu.y = 5
    print("nous sommes maintenant en", milieu)


if __name__ == "__main__":
    manipule_points()

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 fait str(inst) et que inst 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 type str.

Question 4

Dessiner l'état du programme ci-dessous, c'est à dire les variables et instances en mémoire, après exécution de la ligne milieu.y = 5.

Cliquez ici pour révéler la correction. Voici le dessin correct représentant l'état du programme :

mémoire

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 appels Point(..., ...) ;
  • 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 3 : 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__.

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
#!/usr/bin/env python3

"""Programme manipulant des points via une classe"""

from points_classe import Point


class Triangle:
    """Un triangle est composé de trois points"""

    def __init__(self, p1, p2, p3):
        self.points = [p1, p2, p3]

    def __str__(self):
        return str(self.points[0]) + str(self.points[1]) \
            + str(self.points[2])

def test_triangle():
    """Teste la classe triangle"""

    p1 = Point(1, 2)
    p2 = Point(3,4)
    p3 = Point(5,6)
    t1 = Triangle(p1, p2, p3)


if __name__ == "__main__":
    test_triangle()

Question 2

Dessiner l'état du programme ci-dessous, c'est à dire les variables et instances en mémoire, après exécution des quatre lignes.

1
2
3
4
p1 = Point(1,2)
p2 = Point(3,4)
p3 = Point(5,6)
t1 = Triangle(p1, p2, p3)

Cliquez ici pour révéler la correction. Voici le dessin correct représentant l'état du programme :

mémoire

Les points essentiels à retenir de cette question sont les suivants :

  • p1 et t1.points[0] sont deux références vers la même instance ;
  • p2 et t1.points[1] sont deux références vers la même instance ;
  • p3 et t1.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, c'est à dire les variables et instances en mémoire, juste après le dernier appel à la fonction print du 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
#!/usr/bin/env python3

"""Programme à analyser"""

from points_classe import Point
from triangles_classe import Triangle

def fonction_a_analyser():
    """Que fait cette fonction ?"""

    p1 = Point(0 ,0)
    p2 = Point(3, 0)
    p3 = Point(0, 3)
    p4 = Point(0 ,0)
    p5 = Point(4, 0)
    p6 = Point(0, 4)
    t1 = Triangle(p1, p2, p3)
    t2 = Triangle(p4, p5, p6)
    print(t1, t2)
    t1.points[0].x = 5
    print(t1, t2)
    t1.points[0] = p4
    print(t1, t2)
    t1.points[0].x = 5
    print(t1, t2)


if __name__ == "__main__":
    fonction_a_analyser()

Cliquez ici pour révéler la correction. Voici le dessin correct représentant l'état du programme :

mémoire

Les points essentiels à retenir de cette question sont les suivants :

  • la méthode __str__ est appelée automatiquement par l'interpréteur sut t1 et t2 quand on fait print(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 donc t1 et t2 "partagent" un point qui est une instance ⚠️mutable⚠. Donc quand on change l'instance de Point partagée, on change les deux triangles.

Correction vidéo pour l'ensemble de l'exercice :

Exercice 4 : é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
#!/usr/bin/env python3

"""Un petit programme mystère"""

i = 41
j = 42
k = i + 1
print(i is j)
print(i == j)
print(i is k)
print(i == k)
print(j is k)
print(j == k)
print()

a = 1234 * 5678
b = 5678 * 1234
c = b
print(a is b)
print(a == b)
print(a is c)
print(a == c)
print(b is c)
print(b == c)
print()

print(-9 is -9)
print()

r = "41"
s = "42"
t = "4" + str(2)
print(r is s)
print(r == s)
print(r is t)
print(r == t)
print(s is t)
print(s == t)

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 :

mémoire

Voici ce que l'on peut retenir de cet exercice :

  • x is y teste si les deux références x et y pointent vers la même instance ;
  • x == y teste si les deux instances référencées par x et y 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 par x.__eq__(y) voir ici
  • pour les instances immutables, 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 que 7006652 ne l'est pas.

Question 2

Qu'affiche le programme ci-dessous ?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/usr/bin/env python3

"""Un autre programme mystère"""

from points_classe import Point

p1 = Point(42, 42)
p2 = Point(42, 42)
print(p1 == p2)
print(p1 is p2)

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.