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...).
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 |
---|---|
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] 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 |
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 avecA.__len__()
; -
A
(dans un interpréteur) ouprint(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 avecA.__repr__()
; -
B = ["Bonjour", 584] ; A + B
: la dernière instructionA + 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é » deA.__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 typea.__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 typea.__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)
oudir(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) ouint.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 entrerA = [] ; 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 | |
---|---|
Voilà donc le constructeur de notre classe « Personnage » :
|
|
Accesseurs (getters) | |
---|---|
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. |
|
Mutateurs (setters) | |
---|---|
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) :
|
|
Méthodes privées (ou spéciales) | |
---|---|
|
|
Méthodes publiques | |
---|---|
|
|
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.
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.
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 :
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).