Introduction à la programmation orientée objet en Python
Python est un langage qui implémente nativement les paradigmes de la programmation orientée objet (POO). La maîtrise de ces concepts est indispensable pour concevoir des applications modulaires, maintenables et évolutives, notamment dans le domaine de l'intelligence artificielle.
- Notions fondamentales de l'orientation objet
| Concept | Description |
|---|---|
| Classe | Modèle définissant la structure (attributs) et le comportement (méthodes) d'un objet. |
| Objet | Instance concrète d'une classe, possédant un état propre. |
| Encapuslation | Regroupement des données et des opérations, avec masquage de l'implémentation interne. |
| Héritage | Mécanisme permettant à une classe (fille) de réutiliser les propriétés d'une autre classe (mère). |
| Polymorphisme | Capacité d'utiliser une interface unique pour des types d'objets différents. |
| Abstraction | Simplification d'un système en ne montrant que les fonctionnalités essentielles. |
- Création de classes et instanciation d'objets
Syntaxe de base
class NomDeLaClasse:
# Attribut de classe (partagé par toutes les instances)
attribut_partage = "valeur partagée"
# Constructeur : initialise l'état de l'objet
def __init__(self, param1, param2):
self.attribut_instance1 = param1
self.attribut_instance2 = param2
# Méthode d'instance
def afficher(self):
return f"{self.attribut_instance1} - {self.attribut_instance2}"
# Instanciation
objet = NomDeLaClasse("valeur1", "valeur2")
print(objet.afficher()) # résultat : valeur1 - valeur2
Le rôle de __init__
Cette méthode spéciale est automatiquement appelée lors de la création d'un objet. Le paramètre self fait référence à l'instance en cours de création.
class Utilisateur:
def __init__(self, nom, age):
self.nom = nom
self.age = age
def saluer(self):
return f"Bonjour, je m'appelle {self.nom} et j'ai {self.age} ans."
u = Utilisateur("Bob", 30)
print(u.saluer()) # Bonjour, je m'appelle Bob et j'ai 30 ans.
- Encapsulation et contrôle d'accès
Python ne possède pas de modificateurs d'accès stricts comme en Java, mais utilise des conventions de nommmage pour indiquer le niveau de visibilité.
| Convention | Visibilité |
|---|---|
nom |
Public : accessible partout. |
_nom |
Protégé : usage interne recommandé. |
__nom |
Privé : subit un "name mangling" (renommage) pour limiter l'accès directt. |
Exemple pratique
class CompteBancaire:
def __init__(self, proprietaire, solde=0):
self.proprietaire = proprietaire # public
self._solde = solde # protégé
self.__code_pin = "5678" # privé
def deposer(self, montant):
if montant > 0:
self._solde += montant
def get_solde(self, code):
if code == self.__code_pin:
return self._solde
else:
raise ValueError("Code PIN incorrect")
cb = CompteBancaire("Alice", 500)
print(cb.proprietaire) # ✅ accessible
print(cb._solde) # ⚠️ techniquement accessible, déconseillé
# print(cb.__code_pin) # ❌ erreur
print(cb._CompteBancaire__code_pin) # ✅ mais ne doit pas être utilisé
Bonne pratique : Utiliser les propriétés (@property) pour un accès contrôlé.
class Celsius:
def __init__(self, temperature=0):
self._temperature = temperature
@property
def temperature(self):
return self._temperature
@temperature.setter
def temperature(self, valeur):
if valeur < -273.15:
raise ValueError("Température inférieure au zéro absolu impossible")
self._temperature = valeur
c = Celsius(25)
print(c.temperature) # 25
c.temperature = -300 # déclenche une erreur
- Héritage
L'héritage permet de créer une classe dérivée qui hérite des attributs et méthodes de la classe parente.
Héritage simple
class Animal:
def __init__(self, nom):
self.nom = nom
def parler(self):
raise NotImplementedError("Les sous-classes doivent implémenter parler()")
class Chien(Animal):
def parler(self):
return f"{self.nom} aboie !"
class Chat(Animal):
def parler(self):
return f"{self.nom} miaule !"
medor = Chien("Médor")
minou = Chat("Minou")
print(medor.parler()) # Médor aboie !
print(minou.parler()) # Minou miaule !
Appel au parent avec super()
class Employe:
def __init__(self, nom, salaire):
self.nom = nom
self.salaire = salaire
class Manager(Employe):
def __init__(self, nom, salaire, prime):
super().__init__(nom, salaire) # appel au constructeur parent
self.prime = prime
def revenu_total(self):
return self.salaire + self.prime
Héritage multiple
class Volant:
def voler(self):
return "Je vole..."
class Nageur:
nager = lambda self: "Je nage..."
class Canard(Animal, Volant, Nageur):
def parler(self):
return "Coin coin !"
donald = Canard("Donald")
print(donald.voler()) # Je vole...
Attention : L'ordre de résolution des méthodes (MRO) peut être complexe. Utilisez Classe.__mro__ pour le vérifier.
- Polymorphisme
Grâce au duck typing de Python, le polymorphisme est naturel.
def faire_parler(animal: Animal):
print(animal.parler())
animaux = [Chien("Rex"), Chat("Félix"), Canard("Daisy")]
for a in animaux:
faire_parler(a)
# Rex aboie !
# Félix miaule !
# Daisy coin coin !
- Méthodes spéciales (dunder methods)
Ces méthodes, encadrées par des doubles underscores, permettent de personnaliser le comportement des objets.
| Méthode | Rôle |
|---|---|
__str__ |
Représentation lisible pour l'utilisateur (ex: print(obj)) |
__repr__ |
Représentation technique pour le développeur |
__len__ |
Support de la fonction len() |
__getitem__ |
Accès par index (ex: objet[0]) |
__call__ |
Rend l'objet "appelable" comme une fonction |
__eq__ |
Définit l'égalité entre deux objets |
Exemple : conteneur personnalisé
class ListePerso:
def __init__(self, elements=None):
self._elements = elements if elements else []
def __len__(self):
return len(self._elements)
def __getitem__(self, index):
return self._elements[index]
def __str__(self):
return f"ListePerso({self._elements})"
ma_liste = ListePerso([10, 20, 30])
print(len(ma_liste)) # 3
print(ma_liste[1]) # 20
- Méthodes de classe et méthodes statiques
| Type | Décorateur | Premier paramètre | Usage |
|---|---|---|---|
| Méthode d'instance | Aucun | self |
Opère sur l'instance |
| Méthode de classe | @classmethod |
cls |
Opère sur la classe (ex: constructeur alternatif) |
| Méthode statique | @staticmethod |
Aucun | Fonction utilitaire liée logiquement à la classe |
class Personne:
espece = "Homo sapiens"
def __init__(self, nom, age):
self.nom = nom
self.age = age
@classmethod
def depuis_chaine(cls, chaine):
nom, age = chaine.split('-')
return cls(nom, int(age))
@staticmethod
def est_majeur(age):
return age >= 18
p = Personne.depuis_chaine("Alice-28")
print(Personne.est_majeur(16)) # False
- Classes abstraites (ABC)
Elles imposent un contrat aux sous-classes, qui doivent implémenter les méthodes abstraites.
from abc import ABC, abstractmethod
class Forme(ABC):
@abstractmethod
def aire(self):
pass
@abstractmethod
def perimetre(self):
pass
class Rectangle(Forme):
def __init__(self, largeur, hauteur):
self.largeur = largeur
self.hauteur = hauteur
def aire(self):
return self.largeur * self.hauteur
def perimetre(self):
return 2 * (self.largeur + self.hauteur)
# f = Forme() # erreur
r = Rectangle(3, 4)
print(r.aire()) # 12
- Data classes (
@dataclass)
Depuis Python 3.7, ce décorateur simplifie la création de classes principalement destinées à stocker des données.
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
def distance_origine(self):
return (self.x**2 + self.y**2) ** 0.5
pt = Point(3.0, 4.0)
print(pt) # Point(x=3.0, y=4.0)
print(pt.distance_origine()) # 5.0
Les méthodes __init__, __repr__ et __eq__ sont générées automatiquement.
- Principes SOLID (synthèse)
-
Responsabilité unique : Une classe ne doit avoir qu'une seule raison de changer.
-
Ouvert/fermé : Les classes doivent être ouvertes à l'extension, fermées à la modification.
-
Substitution de Liskov : Une sous-classe doit pouvoir remplacer sa classe mère sans altérer le programme.
-
Ségrégation des interfaces : Préférer des interfaces spécifiques plutôt qu'une interface générale.
-
Inversion des dépendances : Dépendre d'abstractions, pas d'implémentations concrètes.
-
Utilisation avancée de
super()
super() ne se limite pas à appeler le constructeur parent. Il gère correctement l'héritage multiple via le MRO.
- Conclusion
La POO en Python offre une flexibilité remarquable pour structurer des applications complexes, notamment en IA. Voici un récapitulatif des mécanismes clés :
| Concept | Implémentation Python |
|---|---|
| Encapsulation | Conventions de nommage + @property |
| Héritage | class Fille(Parent): + super() |
| Polymorphisme | Duck typing + redéfinition de méthodes |
| Abstraction | abc.ABC + @abstractmethod |
| Classes de données | @dataclass |
Recommandations : Favoriser la composition sur l'héritage, utiliser les propriétés pour un contrôle fin, et recourir aux data classes pour les modèles simples.