Chapitre 2 : Introduction à la Programmation Orientée Objet
I. Introduction : Qu'est-ce qu'un objet ?
Jusqu'à présent, la manière de créer des programmes en Python était de créer des fonctions qui interagissent entre elles. On dit qu'on a travaillé avec un paradigme appelé programmation procédurale (qui est un cas particulier de programmation impérative), c'est-à-dire que le programme contient une série d'étapes à réaliser et n'importe quelle procédure (ou fonction) peut être appelée à n'importe quelle étape de l'exécution du programme, y compris à l'intérieur d'autres fonctions et même dans la fonction elle-même (récursivité vue au chapitre précédent).
Cette façon de faire est bien mais elle peut devenir assez vite très (trop) dense et difficile à maintenir : beaucoup de variables "dans tous les sens", beaucoup de fonctions qui font plus ou moins d'actions, etc.
Une autre façon de programmer (ou un autre paradigme) est la Programmation Orientée Objet (POO). La POO est apparue dans les années 60 et est devenue populaire en 1983 avec la sortie du langage de programmation C++. Les programmes ne seront alors plus conçus comme un ensemble de fonctions mais comme un ensemble d'objets qui interagissent les uns avec les autres (ce qui ne veut pas dire qu'il n'y aura plus de fonctions).
Dans la vie réelle, un objet possède des caractéristiques et on peut réaliser des actions sur (ou avec) cet objet. Un crayon, par exemple, possède une taille, une couleur, une forme, etc. qui sont ses caractéristiques et il permet d'écrire (ou de dessiner) sur une feuille (qui a ses propres caractéristiques) ou peut être taillé (avec un objet qui s'appelle un taille-crayon).
En informatique, un objet représente un concept, une idée ou toute chose matérielle ou immatérielle du monde physique, c'est le concept central de la POO. Par exemple, une voiture, une personne, un animal, ou encore un compte bancaire peuvent être vus comme des objets.
Un objet possède une structure interne, un comportement et peut interagir avec d'autres objets. Comme les objets de la vie réelle, les objets informatiques peuvent être très simples ou très complexes.
En POO, on va représenter des objets et leurs relations. L'interaction entre les objets via leurs relations permet de concevoir et réaliser les fonctionnalités attendues et de mieux résoudre les problèmes.
On ne commence pas par coder directement, mais on commence par analyser le programme : que va-t-il faire ? Quelles en sont les grandes composantes ? Quels sont les objets de notre programme et quels sont leurs caractéristiques ? Cette étape de modélisation a une importance majeure et nécessaire pour la POO, elle permet de transcrire les éléments du réel sous forme virtuelle. Elle est appelée l'Analyse Orientée Objet.
Python est un langage orienté objet (c'est loin d'être le seul), ce qui signifie que le langage tout entier est construit autour de la notion d'objets. En fait, quasiment tout en Python est un objet et nous avons manipulé des objets depuis l'année dernière sans forcément nous en rendre compte : les différents types : str, int, list, etc. sont des objets et même les fonctions sont des objets.
Les objectifs principaux de la POO sont de permettre de créer des programmes plus clairs, mieux structurés, plus modulables, plus faciles à maintenir et à déboguer. Elle remplace la programmation procédurale surtout dans les "gros" programmes car elle présente différents avantages :
Chaque type d'objet possède son propre contexte et peut agir avec d'autres suivant des interfaces bien précises. Cela permet d'isoler chaque module et d'en créer séparément de nouveaux qui viendront s'ajouter à l'application. Cette approche est particulièrement employée dans le cas de projets répartis entre plusieurs développeurs.
Une conception de l'algorithme plus claire et organisée. Le programmeur identifie chaque élément de son programme comme un objet ayant son contexte, ses propriétés et des actions qui lui sont propres.
Il existe cependant quelques inconvénients à l'utilisation de la programmation par objet :
Une application orientée objet mal conceptualisée sera difficilement maintenable et modulaire.
La programmation par objet nécessite généralement plus de ressources et de temps d'exécution qu'un code procédural.
Attention, la POO n'est pas synonyme de bonne programmation. Il existe d'excellents développeurs capables de produire du bon code procédural, maintenable, structuré et modulaire et il existe aussi des développeurs en programmation objet qui produisent du mauvais code suite à un manque de conceptualisation.
II. Classe, attribut, méthode.
1. Vocabulaire et définitions.
En POO, un objet est créé à partir d'un modèle appelé classe, dont il hérite les comportements et les caractéristiques. Pour schématiser, on peut dire qu'une classe est une sorte de moule qui permet de créer et dupliquer des objets qui partagent les mêmes caractéristiques.
Une classe sert de plan de création pour un type d'objets. Créer une nouvelle classe correspond à définir un nouveau type d'objets ou un nouveau type de données. La notion de classe permet en quelque sorte de généraliser la notion de "type".
Exemple :
Classe : Car
Attributs : Couleur, Marque, Modèle
3 objets de cette classe créés.
Par exemple, un objet "personne" possède différents attributs qui caractérisent cette personne comme son nom, son prénom, sa couleur des yeux, son sexe, sa couleur des cheveux, sa taille, etc.
- Une classe génère des objets.
- Un objet est une instance d'une classe.
- Attributs : variables contenues dans la classe qui caractérisent l'objet.
- Méthodes : fonctions contenues dans la classe qui caractérisent son comportement (l'ensemble des actions réalisables par l'objet).
Exemple (concret) :
Par exemple, imaginons une classe Voiture qui sert à créer des objets qui sont des voitures (assez logique...). Cette classe peut définir un attribut couleur, un attribut vitesse, un attribut puissance, etc. Ces attributs correspondent à des caractéristiques ou des propriétés qui peuvent réellement exister pour une voiture. La classe Voiture peut également définir des méthodes comme par exemple rouler ou freiner. Une méthode correspond en quelque sorte à une action qui peut être réalisée par cet objet ou sur cet objet.
2. Création d'une classe et d'objets Python.
a. Créer une classe et construire un objet.
Pour créer une classe Python on utilise le mot clé class suivi du nom de la classe. Par convention en Python, le nom identifiant une classe (qu'on appelle aussi son identifiant) débute par une majuscule et on n'utilise pas le caractère "_" pour séparer des mots mais on commence chaque mot par une majuscule (convention dite Camel Case). Ainsi, si on veut créer une classe nommée "ma nouvelle classe" on va la nommer MaNouvelleClasse.
Nous allons commencer, par exemple, par créer une classe qui permet de modéliser une personne ou plus exactement un utilisateur d'un jeu vidéo. Nous allons nommer notre classe User.
On peut trouver de très nombreuses caractéristiques pour une personne mais on ne va en retenir que quelques-unes : le pseudo, le nom, le prénom, l'âge et la ville de résidence. Cela fait donc 5 attributs qui sont les variables internes à notre objet et qui vont le caractériser. Un utilisateur ainsi modélisé sera caractérisé par ces 5 attributs. On peut, bien sûr, en ajouter beaucoup d'autres.
Pour définir les attributs d'un objet, on peut, soit les définir directement dans la classe (mais ce n'est pas une bonne pratique sauf si ce sont des constantes de l'objet), soit les définir à l'aide d'un constructeur dans la classe.
Un constructeur est une méthode (donc une fonction) spéciale, qui est appelée à chaque fois qu'on souhaite créer (ou construire, d'où son nom) un objet depuis la classe. Il porte un nom imposé par le langage Python : __init__(). C'est une méthode se chargeant de créer les attributs de notre objet. Le constructeur peut disposer d'un nombre quelconque de paramètres, éventuellement aucun, et n'a pas de valeur de retour.
Remarque : Les noms de méthodes entourés de part et d'autre de deux signes underscores (comme vu avec __init__()) sont des méthodes spéciales.
Pour créer un objet, il suffit alors de définir son nom et indiquer à quelle classe il appartient. On dit qu'on instancie une classe. On utilise la même notation que pour appeler une fonction.
Exemples :
1. Sans le constructeur, en définissant uniquement les attributs directement dans la classe (ce qui n'est pas une bonne façon de faire) :
class User:
"""on définit un utilisateur caractérisé par 5 attributs"""
user_pseudo = "king_Sacha"
user_nom = "TOUILLE"
user_prenom = "Sacha"
user_age = 17
user_ville = "Paris"
#on instancie l'objet nommé joueur1
joueur1 = User()
#on affiche la valeur de l'attribut user_ville de l'objet joueur1
print(joueur1.user_ville)
Résultat :
Paris
On demande à afficher l'attribut user_ville de l'objet joueur1, Python affiche alors la valeur 'Paris'. pseudo, nom, prenom, etc. sont des attributs de l'objet joueur1.
On utilise le point . pour une relation d'appartenance : joueur1.user_nom est l'attribut user_nom de l'objet joueur1.
joueur1.user_pseudo
Résultat :
'king_Sacha'
Remarque : On peut vérifier qu'un objet est une instance d'une classe avec la fonction isinstance.
Exemple :
isinstance(joueur1, User)
Résultat :
True
2. Avec un constructeur (bonne méthode) sans paramètre :
class User:
"""on définit un utilisateur caractérisé par 5 attributs"""
def __init__(self): #méthode constructeur
self.user_pseudo = "BU_Nice"
self.user_nom = "BU"
self.user_prenom = "Amédé"
self.user_age = 16
self.user_ville = "Nice"
bu = User() #on instancie l'objet bu de la classe User
print(f'pseudo : {bu.user_pseudo}, nom : {bu.user_nom}')
Résultat :
pseudo : BU_Nice, nom : BU
Remarque : Il est vivement conseillé de commencer la définition d'une classe par une docstring (comme pour les fonctions) commentant la classe. Cela devient presque obligatoire pour un gros projet fait à plusieurs personnes.
Dans la définition du constructeur, on passe un premier paramètre nommé self. Ce self reçu en premier (et pour l'instant seul) paramètre par __init__ est l'objet lui-même (self en anglais = soi-même) sur lequel on applique le constructeur.
Le mot self représente une instance de la classe, autrement dit, une méthode a toujours en premier paramètre l'objet lui-même. L'objet qui l'appelle prendra la place de ce paramètre. Par convention, on note ce paramètre self pour bien comprendre que c'est l'objet lui-même qui va être passé en argument. Ce mot n'est pas un mot réservé par Python (contrairement à d'autres langages), on pourrait donc le remplacer par un autre mot (mais il ne faut pas le faire, c'est mal, il faut garder cette convention !).
Enfin, dans le constructeur, nous trouvons l'instanciation des 5 attributs : pseudo, nom, prenom, age et ville. On crée 5 variables self.user_pseudo, self.user_nom, etc. et on leur donne une valeur.
On peut créer autant d'objets que l'on désire avec une classe, on peut, par exemple, créer un autre utilisateur.
Exemple :
otto = User() #on crée l'objet otto de la classe User
print(otto)
Résultat :
<__main__.User object at 0x10fbdfc10>
On constate que si on demande à afficher directement l'objet otto, l'interpréteur affiche un code peu clair, mais qu'il précise la classe dont l'objet est issu. On peut donc vérifier que c'est bien de la classe User dont est issu l'objet otto.
print(otto.user_pseudo)
print(otto.user_nom)
print(otto.user_prenom)
Résultat :
BU_Nice
BU
Amédé
On constate que lors de leur création, les deux objets otto et bu disposent des mêmes attributs avec les mêmes valeurs définies dans la classe, ce qui n'est donc pas pratique. On peut modifier la valeur d'un attribut d'un objet en tapant : objet.attribut = nouvelle_valeur.
Par exemple :
otto.user_pseudo = 'otto_the_best'
otto.user_nom = 'GRAF'
otto.user_prenom = 'Otto'
print(otto.user_pseudo)
print(otto.user_nom)
print(otto.user_prenom)
Résultat :
otto_the_best
GRAF
Otto
Nous avons créé 2 objets avec la classe User mais ces objets ont les mêmes attributs, ce qui est, en pratique, rarement voulu. Généralement, on voudra que les objets disposent déjà d'attributs avec des valeurs qui leur sont propres dès leur création (pourquoi créer 2 objets qu'on veut différents mais finalement identiques lors de leur création, même si on peut les changer ensuite ? Cette façon de faire ne semble pas du tout logique). On va maintenant voir comment fabriquer un constructeur qui prenne plusieurs paramètres pour construire l'objet.
L'idée générale est la suivante : la fonction __init__ va être construite de telle sorte que les arguments passés soient utilisés comme valeur d'initialisation pour les attributs d'un objet. On va passer les arguments lors de l'instanciation de l'objet (sa création) et ces arguments vont être transmis à __init__.
Exemple : Avec un constructeur ayant des paramètres :
class User:
"""on définit un utilisateur caractérisé par 5 attributs"""
def __init__(self, pseudo, nom, prenom, age, ville): #constructeur
self.user_pseudo = pseudo #attributs définis par les paramètres passés
self.user_nom = nom
self.user_prenom = prenom
self.user_age = age
self.user_ville = ville
player1 = User('thebeauty','Onette','Camille',25,'Nice') #on construit un objet avec ses propres attributs
player2 = User('thebeast','Cyla','Eddy',17,'Paris') #on construit un 2ème objet différent du précédent
print(player1.user_prenom)
print(player1.user_nom)
print(player2.user_prenom)
print(player2.user_nom)
Résultat :
Camille
Onette
Eddy
Cyla
self.Remarque : On peut utiliser les arguments du constructeur pour mettre des valeurs par défaut si de nombreux objets ont des caractéristiques communes. On pourra alors instancier des objets "plus facilement" en ne précisant que certaines valeurs.
Exemple :
class Voiture_societe:
"""classe qui caractérise le parc de voitures
d'une société basée à Nice"""
def __init__(self, puissance, couleur = 'grise', carburant = 'E95', marque = 'peugeot', garage = 'Nice'):
self.couleur = couleur
self.carburant = carburant
self.marque = marque
self.puissance = puissance
self.garage = garage
#on crée une voiture en précisant uniquement sa puissance
voiture1 = Voiture_societe(120)
#on précise la puissance et le lieu du garage
voiture2 = Voiture_societe(100, garage = 'St-Laurent')
# on peut aussi écrire :
# voiture2 = Voiture_societe(puissance = 100, garage = 'St-Laurent')
#on précise la marque et la puissance
voiture3 = Voiture_societe(200, marque = 'bmw')
b. Attributs de classe, attributs d'instance et méthodes.
En ajoutant directement une variable dans une classe, on crée ce qu'on appelle un attribut de classe. Les variables créées lors de l'instanciation de l'objet (dans le __init__) sont des attributs de données (ou attributs d'instance).
Les attributs de données stockent des informations relatives à chaque instance alors que les attributs de classe servent à stocker des attributs communs à toutes les instances de la classe.
Pour créer un attribut de classe, il suffit de créer une variable directement dans le corps de la classe, sous la définition, avant le constructeur (en dehors de toute méthode).
Les attributs d'instance sont spécifiques à chaque instance. Pour en créer, on a vu qu'il suffisait de les initialiser dans la méthode __init__.
Exemple :
class Dog:
race = 'canine' #attribut de classe
def __init__(self, nom, age):
self.nom = nom #attribut d'instance
self.age = age #attribut d'instance
Il est considéré comme une bonne pratique de ne créer des attributs de classe que pour définir des variables qui devraient avoir des valeurs constantes à travers les différents objets de la classe lors de leur création.
On a déjà vu que les méthodes correspondent à des actions appliquées sur (ou à) un objet, par exemple, en 1ère, nous avons vu (sans donner d'explication), la méthode .append de la classe list qui permet d'ajouter un élément à l'objet list manipulé.
On a déjà manipulé une méthode "particulière" avec le constructeur __init__, les autres méthodes se définissent de la même façon (mais sans les underscores), ce sont des fonctions qui prennent en premier paramètre l'objet lui-même (donc self) et qui servent à agir sur l'objet. En recevant l'objet en paramètre, elles peuvent en lire et modifier ses attributs (comme avec .append pour les listes).
Exemple :
class User:
"""on définit un utilisateur caractérisé par 6 attributs"""
def __init__(self, pseudo, nom, prenom, age, ville): #constructeur
self.user_pseudo = pseudo
self.user_nom = nom
self.user_prenom = prenom
self.user_age = age
self.user_ville = ville
self.user_anciennete = 0
def level_user(self,n): #méthode qui ajoute n '+' après le pseudo
self.user_pseudo += n * '+'
def anciennete_up(self): #méthode qui ajoute 1 à l'ancienneté d'un joueur
self.user_anciennete += 1
player1 = User('thebeauty', 'Onette', 'Camille', 25, 'Nice')
player2 = User('thebeast', 'Cyla', 'Eddy', 17, 'Paris')
print(player1.user_pseudo)
print(player1.user_anciennete)
player1.level_user(2) #on applique la méthode level_user à l'objet player1
print(player1.user_pseudo)
player1.anciennete_up() #on applique la méthode anciennete_up à l'objet player1
print(player1.user_anciennete)
Résultat :
thebeauty
0
thebeauty++
1
On peut aussi, bien sûr, créer une méthode qui renvoie quelque chose. Par exemple, nous allons ajouter une méthode qui renvoie le nombre de caractères utilisés pour le pseudo (l'utilité réelle de cette méthode est très discutable).
class User:
"""on définit un utilisateur caractérisé par 6 attributs"""
def __init__(self, pseudo, nom, prenom, age, ville): #constructeur
self.user_pseudo = pseudo
self.user_nom = nom
self.user_prenom = prenom
self.user_age = age
self.user_ville = ville
self.user_anciennete = 0
def level_user(self,n): #méthode qui ajoute n '+' après le pseudo
self.user_pseudo += n * '+'
def anciennete_up(self): #méthode qui ajoute 1 à l'ancienneté d'un joueur
self.user_anciennete += 1
def len_pseudo(self): #méthode qui renvoie le nbre de caractères d'un pseudo
return len(self.user_pseudo)
def est_majeur(self): #méthode qui teste si un joueur est majeur
if self.user_age >= 18:
return True
return False
player1 = User('thebeauty', 'Onette', 'Camille', 25, 'Nice')
player2 = User('thebeast', 'Cyla', 'Eddy', 17, 'Paris')
print(player1.len_pseudo())
print(player2.len_pseudo())
print(player1.est_majeur())
print(player2.est_majeur())
Résultat :
9
8
True
False
c. Complément : méthodes pour représenter un objet.
Nous allons voir deux méthodes spéciales (donc avec les underscores comme __init__) qui permettent de contrôler la façon dont l'objet est représenté et affiché par Python. On a déjà vu que quand on instancie des objets issus de nos propres classes, si on essaye de les afficher directement dans l'interpréteur ou grâce à print, on obtient quelque chose d'assez peu parlant comme par exemple : <__main__.User object at 0x112066310>
La première méthode permettant d'améliorer ça est __repr__ qui définit la façon dont est affiché l'objet quand on appelle directement son nom dans l'interpréteur et évite donc d'avoir un affichage comme le précédent.
Exemple :
class User:
"""on définit un utilisateur caractérisé par 6 attributs"""
def __init__(self, pseudo, nom, prenom, age, ville): #constructeur
self.user_pseudo = pseudo
self.user_nom = nom
self.user_prenom = prenom
self.user_age = age
self.user_ville = ville
self.user_anciennete = 0
def __repr__(self): # renvoie la valeur de l'objet validé dans l'interpréteur
return "Pseudo : " + self.user_pseudo + ". Nom : " + self.user_nom
def level_user(self,n): # méthode qui va ajouter des + après le pseudo en fct du niveau du joueur
self.user_pseudo += n * '+'
def anciennete_up(self): # méthode qui ajoute 1 à l'ancienneté d'un joueur
self.user_anciennete += 1
def len_pseudo(self): # méthode qui renvoie le nbre de caractères d'un pseudo
return len(self.user_pseudo)
def est_majeur(self):
if self.user_age >= 18:
return True
return False
player1 = User('thebeauty', 'Onette', 'Camille', 25, 'Nice')
player2 = User('thebeast', 'Cyla', 'Eddy', 17, 'Paris')
print(player1)
Résultat :
Pseudo : thebeauty. Nom : Onette
player2
Résultat :
Pseudo : thebeast. Nom : Cyla
Il existe une seconde méthode spéciale, __str__, utilisée pour afficher l'objet avec la fonction print(). Par défaut, si aucune méthode __str__ n'est définie, Python appelle la méthode __repr__ de l'objet. La méthode __str__ est également utilisée par Python si on veut convertir un objet en chaîne avec le constructeur str (si elle n'est pas définie, là aussi Python utilisera par défaut __repr__).
Exemple :
class User:
"""on définit un utilisateur caractérisé par 6 attributs"""
def __init__(self, pseudo, nom, prenom, age, ville): #constructeur
self.user_pseudo = pseudo
self.user_nom = nom
self.user_prenom = prenom
self.user_age = age
self.user_ville = ville
self.user_anciennete = 0
def __repr__(self): # renvoie la valeur de l'objet validé dans l'interpréteur
return "Pseudo : " + self.user_pseudo + ". Nom : " + self.user_nom
def __str__(self): #affiche l'objet avec print
return "L'utilisateur a pour pseudo :" + self.user_pseudo + " et pour nom : " + self.user_nom
player1 = User('thebeauty', 'Onette', 'Camille', 25, 'Nice')
print(player1)
Résultat :
L'utilisateur a pour pseudo :thebeauty et pour nom : Onette
Remarques :
Il existe aussi une méthode spéciale pour détruire un objet, la méthode spéciale
__del__qui est appelée au moment de la destruction de l'objet (notamment via l'instructiondel). Par défaut, cela supprime "simplement" l'objet qui a été créé, mais on peut ajouter, par exemple, un message :def __del__(self): print('RIP')Il existe encore de très nombreuses autres méthodes spéciales en Python mais nous ne les détaillerons pas dans ce cours qui reste une introduction à la programmation orientée objet en Python, et notamment les notions d'encapsulation et d'héritage ne seront pas abordées.