Programmation Orientée Objet (POO) en Python#
Jusqu'à présent, nous avons principalement adopté une approche procédurale : notre code était composé de fonctions indépendantes recevant des données brutes en paramètres.
La Programmation Orientée Objet (POO) propose un changement de perspective fondamental : elle regroupe au sein d'une même entité logique appelée Objet les données qui le caractérisent (ses attributs) et les actions qu'il peut accomplir (ses méthodes).
En Python, la POO n'est pas une surcouche optionnelle : absolument tout en Python est un objet (les nombres, les chaînes, les listes, et même les fonctions !).
1. Classes vs Objets : Le moule et la matière#
Pour comprendre l'articulation entre une classe et un objet :
flowchart TD
A["Classe CompteBancaire : Le Plan d'Architecte (Moule)"] -->|Instanciation 1| B["Objet compte_alice : (Solde = 2500 EUR)"]
A -->|Instanciation 2| C["Objet compte_bob : (Solde = 1400 EUR)"]
- La Classe (
class) : c'est le plan d'architecte, le modèle abstrait qui définit les caractéristiques communes de tous les futurs objets. - L'Objet (ou Instance) : c'est l'entité concrète matérialisée en mémoire vive, créée à partir du modèle de la classe.
2. Définition d'une classe et le constructeur __init__#
On déclare une classe avec le mot-clé class (en convention PascalCase), et on initialise ses attributs d'instance dans la méthode spéciale __init__ :
class CompteBancaire:
"""Modèle représentant un compte bancaire client."""
def __init__(self, titulaire: str, solde_initial: float = 0.0):
# Attributs d'instance (spécifiques à chaque compte)
self.titulaire = titulaire
self.solde = solde_initial
# Méthode d'instance
def deposer(self, montant: float) -> None:
if montant > 0:
self.solde += montant
print(f"Depot de {montant:.2f} EUR effectue. Nouveau solde : {self.solde:.2f} EUR")
else:
print("Erreur : Le montant du depot doit etre positif.")
def retirer(self, montant: float) -> bool:
if 0 < montant <= self.solde:
self.solde -= montant
print(f"Retrait de {montant:.2f} EUR effectue. Nouveau solde : {self.solde:.2f} EUR")
return True
else:
print("Erreur : Provision insuffisante pour ce retrait.")
return False
# Instanciation de deux comptes indépendants
compte_alice = CompteBancaire("Alice Martin", 1500.0)
compte_bob = CompteBancaire("Bob Diop")
compte_alice.deposer(500.0) # Solde passe à 2000.0 EUR
compte_alice.retirer(2500.0) # Échec provision
Le rôle fondamental du paramètre self#
[!IMPORTANT] En Python, le premier paramètre de toute méthode d'instance est obligatoirement
self.
selfreprésente une référence explicite vers l'instance précise sur laquelle la méthode est invoquée.
Lorsque vous écrivezcompte_alice.deposer(500.0), Python traduit automatiquement cet appel enCompteBancaire.deposer(compte_alice, 500.0).
3. Les méthodes magiques indispensables : __str__ et __repr__#
Par défaut, afficher un objet avec print(compte_alice) affiche une adresse mémoire brute peu lisible comme <__main__.CompteBancaire object at 0x7f8a12>.
Pour offrir un rendu élégant, on surcharge deux méthodes dunder (double underscore) :
- __str__(self) : représentation textuelle conviviale destinée à l'utilisateur final (print, str()).
- __repr__(self) : représentation technique non ambiguë destinée au développeur et au débogage.
class CompteBancaire:
def __init__(self, titulaire: str, solde: float = 0.0):
self.titulaire = titulaire
self.solde = solde
def __str__(self) -> str:
return f"Compte de {self.titulaire} (Solde : {self.solde:.2f} EUR)"
def __repr__(self) -> str:
return f"CompteBancaire(titulaire='{self.titulaire}', solde={self.solde})"
compte = CompteBancaire("Alice", 2500.0)
print(compte) # Déclenche __str__ : Compte de Alice (Solde : 2500.00 EUR)
print(repr(compte)) # Déclenche __repr__: CompteBancaire(titulaire='Alice', solde=2500.0)
4. Attributs d'instance vs Attributs de classe#
- Attribut d'instance : rattaché à une instance individuelle via
self.x. Chaque objet a sa propre copie. - Attribut de classe : défini directement dans le corps de la classe. Il est partagé par toutes les instances créées !
class Etudiant:
# Attribut de classe partagé par tous les étudiants
etablissement = "AlgoJungle Academy"
compteur_inscrits = 0
def __init__(self, nom: str):
# Attribut d'instance propre à cet étudiant
self.nom = nom
Etudiant.compteur_inscrits += 1
e1 = Etudiant("Koffi")
e2 = Etudiant("Sarah")
print("Etablissement :", Etudiant.etablissement)
print("Nombre total d'inscrits :", Etudiant.compteur_inscrits) # 2
5. Encapsulation et le décorateur @property#
Contrairement à d'autres langages qui disposent de mots-clés stricts private ou public, Python adopte la philosophie du consentement mutuel : « We are all consenting adults here ».
- Un tiret bas préfixé
_attributsignale une convention d'attribut interne protégé. - Deux tirets bas
__attributdéclenchent le masquage de nom (Name Mangling).
Pour protéger un attribut tout en permettant d'y accéder comme une variable naturelle sans écrire de lourds getters/setters (get_prix(), set_prix()), on utilise le décorateur @property :
class Article:
def __init__(self, designation: str, prix: float):
self.designation = designation
self.prix = prix # Déclenche automatiquement le setter ci-dessous !
@property
def prix(self) -> float:
"""Getter : retourne le prix."""
return self._prix
@prix.setter
def prix(self, nouvelle_valeur: float) -> None:
"""Setter : valide la cohérence des données avant modification."""
if nouvelle_valeur < 0:
raise ValueError("Le prix d'un article ne peut pas etre negatif !")
self._prix = nouvelle_valeur
produit = Article("Clavier RGB", 49.90)
print(f"Prix : {produit.prix} EUR")
# Tentative de saisie incohérente :
try:
produit.prix = -15.0 # Bloqué par le setter !
except ValueError as e:
print("Alerte de securite :", e)
6. Défi d'application#
Développez une classe Rectangle prenant à l'initialisation une longueur et une largeur. Implémentez deux propriétés en lecture seule (@property) calculant dynamiquement l'aire et le perimetre, ainsi que la méthode spéciale __eq__ permettant de comparer si deux rectangles ont des aires identiques avec l'opérateur ==.
Commentaires