| |
Les nouvelles technologies pour l’enseignement des mathématiques
Intégration des TICE dans l’enseignement des mathématiques

MathémaTICE, première revue en ligne destinée à promouvoir les TICE à travers l’enseignement des mathématiques.

Python 3, un bel objet ?

Le titre de l’article est un clin d’œil à la Programmation Orientée Objet (POO)
Fabrice Houpeaux élabore une classe Matrice en Python 3, et en montre les utilisations (instanciation, opérations, tests d’appartenance et d’égalité, affichages...).

Article mis en ligne le 5 mars 2022
dernière modification le 24 avril 2022

par Fabrice Houpeaux

Cet article peut être librement diffusé et son contenu réutilisé pour une utilisation non commerciale (contacter les auteurs pour une utilisation commerciale) suivant la licence CC-by-nc-sa

On peut visionner les deux vidéos en plein écran en cliquant en bas à droite du lecteur multimédia.

Le titre de l’article est un clin d’œil à la Programmation Orientée Objet (POO)

Introduction de l’article

Je présente dans la vidéo ci-dessous les deux objectifs principaux de cet article :

Notions de paradigmes de programmation

On peut construire un algorithme en le pensant essentiellement en programmation impérative, en programmation fonctionnelle, en programmation orientée objet (POO)... Ou autres. Ce sont ces approches différentes qu’on appelle des paradigmes de programmation. Quelques précisions :

  • un paradigme n’est pas meilleur qu’un autre, mais on n’aurait pas inventé un paradigme si cela n’était pas utile pour résoudre certains problèmes ;
  • avec un même langage de programmation, on peut souvent utiliser des paradigmes différents ;
  • dans un même programme, on peut utiliser des paradigmes différents ;
  • un même objectif peut se réaliser avec différents paradigmes sans qu’un paradigme soit réellement plus efficace qu’un autre, parfois un paradigme se prête mieux à l’objectif ;
  • en mathématiques, un problème issu de la géométrie peut se traiter par la géométrie, il arrive qu’on l’approche plus facilement par l’analyse ou les probabilités. Pour les paradigmes de programmation, c’est un peu la même chose : cela étend le champ des possibles.

Un peu de vocabulaire en POO

Je vais utiliser une copie d’écran d’un interpréteur ipython3 :

ipython3 Premiers vocabulaires
Classes classiques A est un objet de type list. Au niveau du vocabulaire, list s’appelle une classe et A s’appelle un objet de cette classe : on dit que A est une instanciation de la classe list, que 2 est une instanciation de la classe int, que « b » est une instanciation de la classe str et que True est une instanciation de la classe bool.

Une classe définit notamment les attributs (les caractéristiques) et les méthodes (les fonctionnalités) de ses instanciations.

A = [2, "b", True]
dir(A)

Je vous invite à faire interpréter rapidement ces deux instructions. La dernière instruction vous affiche les méthodes de la classe list, du moins celles que les créateurs de la classe ont décidé de rendre visible via la méthode __dir__. Vous obtiendriez le même type de sortie avec l’instruction A.__dir__() : la méthode __dir__() définit le comportement de l’instruction dir().
Notons au passage la syntaxe de l’appel d’une méthode sur un objet nom_objet.nom_methode(arguments).

Quelques exemples d’utilisation de méthodes

  • A.append(2022) : utilisation de la méthode append() de la classe list dont le rôle est d’ajouter l’élément donné en argument à la fin de la liste A ;
  • len(A) : appel « caché » de la méthode __len__() afin de retourner le nombre d’éléments de la liste, on peut considérer que cette méthode définit le comportement de len() et retourne un attribut d’une liste (son nombre d’éléments). Vous obtiendriez le même retour avec A.__len__() ;
  • A (dans un interpréteur) ou print(A) (instruction programme) : appels « cachés » des méthodes __repr__() ou __str__() pour l’affichage souhaité d’une instanciation de la classe list. Vous obtiendriez le même genre de retour avec A.__repr__() ;
  • B = ["Bonjour", 584] ; A + B : la dernière instruction A + B appelle la méthode __add__() via A.__add__(B) (cette méthode définit le comportement de l’utilisation du symbole « + » entre deux instanciations de la classe list et c’est une concaténation non commutative), puis la méthode __repr__() pour l’affichage de la liste A + B dans un interpréteur ;
  • A[0] : cette instruction vous affichera « 2 » dans un interpréteur via un appel « caché » de A.__getitem__(0) (pour retourner la valeur), puis un appel de la méthode __repr__() (mais de la classe int) ;
  • a = 5 ; b = 8 ; a + b : la dernière instruction affichera « 13 » via un appel de la méthode __add__() de la classe int du type a.__add__(b). Cette méthode définit le comportement de l’utilisation du symbole « + » entre deux instanciations de la classe int et cela correspond à l’addition commutative de deux entiers ;
  • a = 5 ; b = 8 ; a * b : la dernière instruction affichera « 40 » via un appel de la méthode __mul__() de la classe int du type a.__mul__(b). Cette méthode définit le comportement de l’utilisation du symbole « * » entre deux instanciations de la classe int et cela correspond à la multiplication de deux entiers. Notez que dans la classe list, vous n’avez pas de méthode __mul__() ;
  • dir(int) ou dir(a) : liste les méthodes visibles de la classe int ;
  • a = 2022 ; a.bit_length() : la dernière instruction retournera « 11 », le nombre de bits de la représentation binaire en machine de « 2022 » ce qu’on peut considérer comme un attribut de la classe int ;
  • une fois les méthodes d’une classe listées, vous pouvez obtenir des informations sur une méthode particulière : help(int.bit_length) (tout interpréteur) ou int.bit_length? (ipython3) ;
  • certaines méthodes ont une nomenclature particulière avec « des doubles underscores » avant et après le nom de la méthode, nous en parlerons très bientôt parce qu’il conviendra de parler du principe d’encapsulation en POO.

Ce que cet article n’est pas et ce qu’il est peut-être

  • cet article n’est pas un cours de POO. Certains concepts de la POO ne seront pas traités et les concepts utilisés seront expliqués en partie par l’exemple. Un compromis a été choisi entre la théorie et la mise en application rapide ;
  • certains choix du langage Python 3 quant à l’implémentation de la POO seront abordés, choix qui, au départ, peuvent perturber des utilisateurs de la POO dans d’autres langages. Python 3 sera le seul langage utilisé pour les programmes ;
  • la POO n’a de limite que notre imagination ; la POO, c’est pratique ; la POO, c’est rigolo et ce n’est pas forcément compliqué (la preuve, j’en fais). Expliciter en partie ces derniers points est aussi un des objectifs de cet article ;
  • le principe d’encapsulation et l’utilisation d’un nom de variable comme « self » dans la POO en Python 3 sont des notions fondamentales, c’est pour cela qu’il m’a semblé inévitable de s’y attarder un petit peu ;
  • cet article discute des premiers fondamentaux de la POO (syntaxe d’implémentation en Python 3, bonnes pratiques) et permet éventuellement d’être rapidement autonome ;
  • la POO est au programme de terminale NSI. On l’utilise sur les programmes de lycée en sciences avec Python 3 puisqu’on travaille avec des classes sans forcément le savoir. Un enseignant peut très bien construire ou récupérer des classes Matrice, Suite ou autres et les fournir aux élèves pour qu’ils les utilisent (avoir un petit recul sur ce qu’on utilise ou sur ce qu’on fait utiliser n’est jamais inutile, notamment pour le choix du vocabulaire). On peut imaginer des élèves de terminales NSI investis d’une mission de construction de classe, classe qui serait utilisée sur un autre niveau et dans une autre matière.

    Note constructif d’un relecteur : le langage Scratch utilisé par les élèves depuis le collège permet la programmation orientée objets.

Premier résumé de POO

  • une classe définit un moule (un objet A d’une classe B est construit à partir du moule de la classe B) ;
  • lorsqu’on crée un objet A d’une classe B, on dit qu’on a instancié la classe B ou que A est une instance de la classe B ;
  • une classe définit des attributs et des méthodes dont toutes les instances de cette classe disposeront ;

Le principe d’encapsulation en POO

Utilisateur d’une classe et pilote d’avion

Le concepteur d’une classe a souvent englobé dans celle-ci un code qui peut être assez complexe et il est donc inutile, voire dangereux de laisser un utilisateur de cette classe manipuler ce code sans aucune restriction. Il est important d’interdire à l’utilisateur de la classe de modifier directement certains attributs d’un objet ou d’utiliser certaines méthodes. C’est ce qu’on appelle le principe d’encapsulation : l’encapsulation garantit ainsi la validité des types et des valeurs des données des objets.

En Python 3 par exemple, vous utilisez la classe list, mais vous ne pouvez pas modifier l’attribut « len » d’une liste directement. Cet attribut est éventuellement modifié de l’intérieur de la classe, mais pas de l’extérieur : quand vous appelez la méthode append() sur une liste, cette méthode appellera d’autres méthodes de la classe list qui agiront à l’intérieur de la classe afin de modifier les attributs de votre objet, c’est le principe des méthodes publiques et des méthodes privées. Une méthode publique peut être appelée de l’extérieur de la classe (directement sur un objet, une instance de la classe), une méthode privée est appelée de l’intérieur de la classe.

On peut faire une analogie de ce principe d’encapsulation avec un avion où sont disponibles des centaines de boutons. Ces boutons constituent des actions que l’on peut effectuer sur l’avion et représentent une interface entre les composants de l’avion et le pilote. Le rôle du pilote est de piloter et il va se servir des boutons afin de manipuler les composants de l’avion, mais le pilote ne modifiera pas manuellement ces composants : il pourrait faire de grosses bêtises.

L’interface entre les attributs d’une instance et l’utilisateur de la classe sont les méthodes publiques : l’utilisateur d’une classe doit se contenter d’en invoquer les méthodes publiques en ne modifiant pas directement les attributs.

Sur ce principe d’encapsulation, Python 3 est un peu particulier en tant que langage orienté objet. On ne parle pas de méthodes privées, on parle de méthodes spéciales. Vous pouvez par exemple appeler vous-même ces méthodes spéciales de l’extérieur de la classe ce qui, généralement, n’est pas possible dans les autres langages orientés objet. Attention : même si c’est possible en Python 3, il convient de ne pas le faire, c’est justement indiqué par les « __ » dans le nom de la méthode. En Python 3, vous pouvez aussi directement accéder aux attributs et modifier leurs valeurs ce qui n’est pas possible dans les autres langages orientés objet. Concernant ce dernier point, il convient aussi de ne pas le faire.

Nous y reviendrons : Python 3 est très souple dans le principe d’encapsulation, cela a des avantages et des inconvénients.

Quelques précisions sur les méthodes

  • append() et pop() sont des méthodes publiques de la classe list ;
  • __getitem__() et __len__() sont des méthodes spéciales de la classe list ;
  • la méthode spéciale __init__() a un rôle bien particulier, c’est le constructeur de la classe. Par exemple, quand on entre A = [10, "bo", 78] , ce constructeur est appelé et appellera lui-même d’autres méthodes de la classe. Je ne m’étends pas pour l’instant : sans constructeur, vous devriez entrer A = [] ; A.append(10) ; A.append("bo") ; A.append(78) afin d’obtenir le même résultat.

Deuxième résumé de POO

  • parmi les méthodes d’une classe, il y a des méthodes publiques qui peuvent être appelées avec la syntaxe nom_objet.nom_methode(arguments), on dit qu’on peut les appeler de l’extérieur de la classe ; il y a aussi des méthodes privées (méthodes spéciales en Python 3) qui ne sont pas faites pour être appelées directement par l’utilisateur de la classe, on dit qu’on ne peut les appeler que de l’intérieur de la classe ;
  • une classe peut implémenter une méthode spéciale appelée constructeur qui permet d’instancier un objet de la classe avec des valeurs données pour certains attributs.

Un exemple avec une classe « Personnage »

Construction de la classe

Construire et faire interagir des personnages est un exemple parmi d’autres afin d’introduire la POO.

Constructeur __init__() de la classe Personnage
  1. class Personnage:
  2.  
  3.     # utilisation d'un constructeur
  4.     def __init__(self, force, localisation, experience, degats):
  5.         """Construction d'un personnage : A = Personnage(force(int),
  6.        localisation(str), experience(int), degats(int))"""
  7.         self.setForce(force)
  8.         self.setLocalisation(localisation)
  9.         self.setExperience(experience)
  10.         self.setDegats(degats)

Télécharger

Voilà donc le constructeur de notre classe « Personnage » :

  • vous avez la syntaxe d’implémentation d’une classe ; par convention, on met souvent une majuscule pour le nom d’une classe ;
  • vous avez un « docstring » pour la fonction constructeur avec les « «  »" », c’est un texte supplémentaire qui sera affiché lorsqu’on passe le nom de la méthode à la fonction help() dans un interpréteur ;
  • le nom de variable « self » représente l’instance de la classe, c’est un point important et incontournable de la POO en Python 3. À l’intérieur de la classe, « self » permet de cibler les attributs de l’instance en cours de traitement, d’appliquer des méthodes sur l’instance en cours de traitement ;

    Note constructif d’un relecteur : « self » n’est pas un mot-clé en Python 3, vous pourriez choisir un autre nom de variable (on choisit souvent « self » en Python 3 et c’est le nom qui sera utilisé dans cet article). C’est aussi pour cela que vous devez mettre ce nom de variable en argument lors de la définition de la méthode dans votre classe en Python 3. Par exemple, en PHP, vous n’avez pas à mettre ce nom de variable en argument, le nom de variable utilisé est « $this » et c’est un mot-clé dans ce cas (on parle plutôt de « pseudo-variable »).

  • En Python 3, une méthode impactant une instance en cours de traitement doit contenir l’argument « self » dans sa définition (le constructeur aussi). On comprendra mieux avec les parties suivantes de la classe, mais on créera une instance de Personnage par l’instruction Arianna = Personnage(20, "Quito", 50, 12) ; le constructeur va « travailler » avec Arianna comme instance et appeler les méthodes
    Arianna.setForce(force)
    Arianna.setLocalisation(localisation)
    ...
  • il convient bien d’avoir une pseudo-variable ou un nom de variable pour cibler l’instance en cours de traitement à l’intérieur de la classe, c’est même une idée géniale ;
  • notez aussi que « self » est utilisé comme argument lors de la définition d’une méthode agissant sur l’instance en cours de traitement, mais on ne l’utilise pas comme argument lorsque la méthode est appelée de l’intérieur ou de l’extérieur de la classe ;
  • si c’est encore un peu flou, cela ne le sera plus très rapidement :-)
Accesseurs (getters)
  1. # Méthodes accesseurs (getters).
  2.     def getForce(self):
  3.         return self._force
  4.  
  5.     def getLocalisation(self):
  6.         return self._localisation

Télécharger

Le principe d’encapsulation nous empêche d’accéder directement aux attributs de notre objet puisqu’ils sont censés être privés la plupart du temps : seule la classe peut les lire et les modifier. Par conséquent, si vous voulez récupérer la valeur d’un attribut d’une instance, il va falloir le demander à la classe, de même si vous voulez modifier la valeur d’un attribut. Ces actions vont se faire à l’aide de méthodes publiques qu’on appelle les accesseurs (getters en anglais) et les mutateurs (setters en anglais). Dans le code ci-dessus, vous avez deux exemples d’implémentations d’accesseurs en Python 3.

  • il est bon de choisir une même convention d’écriture pour le nom des accesseurs ;
  • notez le « _ » devant le nom de l’attribut « _force » qui indique que cet attribut est d’ordre privé (en Python 3, c’est une préconisation puisque vous pouvez tout de même accéder à sa valeur sans passer par l’accesseur, mais dans la plupart des autres langages implémentant la POO, vous ne pourrez pas parce qu’il y a l’utilisation de mot-clés du type « public » et « private » pour les attributs et les méthodes). On pourrait mettre ici un docstring afin d’expliquer le rôle de chaque accesseur (pas forcément utile) ;
  • au niveau de l’utilisation de la classe
    Tony = Personnage(20, "Quito", 50, 12)
    Tony.getForce()

    La dernière instruction vous retournera la valeur de l’attribut « _force » de l’instance Tony de la classe Personnage ;

  • comprenez-bien la double utilisation de « self » dans la définition de l’accesseur (comme argument de la méthode puisqu’on définit ici l’accesseur et dans le retour de la méthode puisqu’on veut récupérer un attribut de l’instance en cours de traitement).
Mutateurs (setters)
  1. # Méthodes mutateurs avec assertion (setters)
  2.     def setForce(self, valeur):
  3.         assert (isinstance(valeur, int) and valeur >= 0 and valeur <= 100), \
  4.             "Votre force ne convient pas !"
  5.         self._force = valeur
  6.  
  7.     def setLocalisation(self, valeur):
  8.         assert (isinstance(valeur, str) and len(valeur) >= 3 and len(valeur) <= 20), \
  9.             "Votre localisation ne convient pas !"
  10.         self._localisation = valeur

Télécharger

Comment cela se passe-t-il si vous voulez modifier la valeur d’un attribut privé ? Vous allez demander à la classe de le faire pour vous. Le principe d’encapsulation est là pour vous empêcher d’assigner un mauvais type de valeur à un attribut privé : si c’est la classe qui s’en occupe, ce risque est supprimé car la classe « contrôle » la valeur des attributs. Ce sera donc par le biais de méthodes que l’on demandera à la classe de modifier un attribut privé. Ces méthodes s’appellent des mutateurs (setters en anglais) :

  • il est bon de choisir une même convention d’écriture pour le nom des mutateurs ;
  • encore une fois, « self » est doublement utilisé ;
  • « assert » est utilisé pour le test de la valeur de l’attribut à modifier ; cette instruction est vraiment pratique :
    • la syntaxe est plutôt simple assert (condition), "message d'erreur". Si la condition retourne le booléen True, on passe à l’instruction à la suite du assert, sinon le programme s’arrêtera avec l’affichage du message d’erreur (on dit qu’on a levé une exception) ;
    • pour un mutateur et la vérification des conditions, assert est plus conseillé que des « if » et des « else » ;
    • « assert » peut servir en production (comme ici), en développement (pour déboguer), en tests d’une fonction (avec un jeu de tests fourni, je le fais souvent avec mes élèves)...
  • au niveau de l’utilisation de la classe
    Tony = Personnage(20, "Quito", 50, 12)
    Tony.setForce(35.8)

    Une exception sera levée et le programme sera stoppé.


    Tony = Personnage(20, "Quito", 50, 12)
    Tony.setForce(35)

    Aucune exception levée et le programme continue.


    C’est plutôt « propre » comme fonctionnement .

  • en Python 3, le code ci-dessous ne lèvera pas d’exception ou d’erreur, mais il convient de ne pas le faire parce qu’on viole le principe d’encapsulation et la classe n’est pas censée être utilisée comme cela.
    Arianna = Personnage(20, "Cuenca", 50, 12)
    Arianna._force = 35

    La plupart du temps, un attribut d’instance d’une classe est privé. Comme déjà dit, on utilise des accesseurs et des mutateurs pour le manipuler, la plupart des langages implémentant la POO lèveront une exception pour le code ci-dessus.

Méthodes privées (ou spéciales)
  1. # Méthodes privées de la classe
  2.     def __gagner_experience__(self, exp):
  3.         assert (exp in [5, 10, 15]), \
  4.             "Les seuls gains d'expériences possibles sont 5, 10 ou 15."
  5.         self.setExperience(self._experience + exp)
  6.  
  7.     def __mettre_degats__(self, Perso):
  8.         Perso.setDegats(Perso._degats + 5)

Télécharger

  • l’utilisation de la convention « __ » vous indique que c’est une méthode privée (ou spéciale en Python 3) et qu’on ne doit pas l’appeler de l’extérieur de la classe avec une instruction du type Tony.__gagner_experience__(10). Ici, c’est une méthode qui sera appelée par une des méthodes publiques ci-dessous ;
  • l’utilisation de « self » et de l’instruction « assert » sont classiques ;
  • j’attire éventuellement votre attention sur les instructions du type self.setExperience(self._experience + exp). On appelle les mutateurs afin de modifier la valeur d’un attribut, c’est bien mieux que self._experience = self._experience + exp ; en effet, dans ce dernier cas, une valeur d’expérience supérieure à 100 ne lèvera pas d’exception. Même de l’intérieur de la classe, on passe par les mutateurs pour modifier la valeur d’un attribut.
Méthodes publiques
  1.     # Méthodes publiques de la classe
  2.     def parler(self, phrase):
  3.         assert (isinstance(phrase, str)), \
  4.             "Votre phrase n'en est pas une !"
  5.         print(phrase)
  6.  
  7.     def frapper(self, Perso):
  8.         assert (isinstance(Perso, Personnage)), \
  9.             "Vous ne pouvez frapper qu'un personnage !"
  10.         self.__gagner_experience__(10)
  11.         self.__mettre_degats__(Perso)

Télécharger

  • ces méthodes peuvent être appelées de l’extérieur de la classe avec des instructions du type Arianna.parler("Bonjour.") ou Arianna.frapper(Tony) ;
  • frapper(self, perso) appelle des méthodes spéciales ;
  • dans l’instruction self.__mettre_degats__(Perso), self est le personnage qui frappe et Perso est le personnage frappé.

Bien évidemment, cette classe est incomplète et imparfaite. C’est juste un exemple afin de mettre en application la plupart des propos précédents.

Utilisation de la classe

Le fichier programme contenant la construction de la classe Personnage s’appelle construction_classe_personnage.py. Vous pouvez importer une classe comme vous importez des fonctions d’un module : from construction_classe_personnage import Personnage.
Afin de tester et d’utiliser, il est préférable de mettre les deux fichiers dans le même répertoire.
Vous pouvez aussi ouvrir un interpréteur Python 3, vous placer dans le répertoire du fichier de la classe et importer la classe comme ci-dessus.
Vous avez les deux fichiers ci-dessous.

Classe Personnage

Une classe « Matrice »

Mise à disposition de la classe implémentée en Python 3

Pour cette classe « Matrice », le choix a été fait de conserver l’initialisation des indices des lignes et des colonnes d’une matrice à 1 (il me semble que c’est plutôt la convention en mathématiques). Au niveau implémentation, c’est discutable puisque la classe utilise les listes de Python 3 dont les indices sont initialisés à 0. Il eût été plus simple d’initialiser tous les indices à 0, mais ce n’est pas ce que j’ai fait afin de conserver l’initialisation usuelle des indices d’une matrice.

L’implémentation de ma classe Matrice est disponible ci-dessous.

Classe Matrice

Instanciations de la classe matrice

  • une vidéo afin de montrer l’instanciation de la classe, d’expliciter certains choix dans cette implémentation et dans le principe d’encapsulation en Python 3 (il y a quelques petites redondances au début avec la vidéo d’introduction, mais cela ne fait pas de mal :-)) :
  • Utilisation des matrices

Voir la vidéo d’introduction.

  • Spécification du TAD

La spécification de base utilisée afin d’implémenter la classe Matrice en Python 3 :

TAD Matrice

Conclusions succinctes

Je remercie grandement l’équipe de relecture et de rédaction de la revue pour leurs précieux conseils quant à certains points de cet article.

Tous les codes de cet article sont faits pour être réutilisés, améliorés, complétés. Ils sont donc « quasi-totalement » libres dans leurs utilisations et leurs distributions (voir les conditions en haut de l’article).