La POO en Python
Qu'est-ce que la POO ?
La Programmation Orientée Objet (POO) est un paradigme de programmation qui organise le code autour d'objets plutôt que de fonctions. Un objet regroupe des données (attributs) et des comportements (méthodes) dans une seule entité.
Classes vs Objets
- Une classe est le moule, le plan de construction
- Un objet est une instance concrète créée à partir de ce moule
# La classe est le plan
class Voiture:
pass
# Les objets sont des instances concretes
ma_peugeot = Voiture()
ta_renault = Voiture()
Attributs vs Méthodes
- Les attributs sont les données de l'objet (nom, age, couleur...)
- Les méthodes sont les fonctions de l'objet (rouler, freiner, klaxonner...)
Définir une classe : class et PascalCase
En Python, les classes s'écrivent avec le mot-clé class et en PascalCase (chaque mot commence par une majuscule) :
class Animal: # Bon : PascalCase
pass
class VoitureElectrique: # Bon : PascalCase
pass
# class voiture: # Mauvais : minuscules
# class voiture_electrique: # Mauvais : snake_case
Le constructeur __init__ et self
La méthode __init__ est le constructeur : elle est appelée automatiquement quand on crée un nouvel objet.
Le paramètre self représente l'instance elle-même. Il doit toujours être le premier paramètre de toute méthode d'instance.
class Personne:
def __init__(self, nom, age):
self.nom = nom # Attribut d'instance
self.age = age # Attribut d'instance
# Creation d'instances
alice = Personne("Alice", 30)
bob = Personne("Bob", 25)
print(alice.nom) # Alice
print(bob.age) # 25
Attributs d'instance vs attributs de classe
class Chien:
# Attribut de CLASSE : partagé par toutes les instances
espece = "Canis lupus familiaris"
nombre_pattes = 4
def __init__(self, nom, race):
# Attributs d'INSTANCE : propres a chaque objet
self.nom = nom
self.race = race
rex = Chien("Rex", "Berger allemand")
luna = Chien("Luna", "Labrador")
print(rex.nom) # Rex (attribut d'instance)
print(luna.nom) # Luna (attribut d'instance)
print(rex.espece) # Canis lupus familiaris (attribut de classe)
print(Chien.espece) # Canis lupus familiaris (accessible depuis la classe aussi)
# Modifier un attribut de classe affecte TOUTES les instances
Chien.espece = "Canis familiaris"
print(luna.espece) # Canis familiaris
Méthodes d'instance
Les méthodes d'instance prennent toujours self comme premier paramètre. Elles peuvent accéder et modifier les attributs de l'instance.
class CompteBancaire:
def __init__(self, titulaire, solde_initial=0):
self.titulaire = titulaire
self.solde = solde_initial
def deposer(self, montant):
if montant > 0:
self.solde += montant
print(f"Depot de {montant}. Nouveau solde : {self.solde}")
def retirer(self, montant):
if montant > self.solde:
print("Fonds insuffisants")
else:
self.solde -= montant
print(f"Retrait de {montant}. Nouveau solde : {self.solde}")
def afficher_solde(self):
print(f"Compte de {self.titulaire} : {self.solde}")
compte = CompteBancaire("Alice", 1000)
compte.deposer(500) # Depot de 500. Nouveau solde : 1500
compte.retirer(200) # Retrait de 200. Nouveau solde : 1300
compte.afficher_solde() # Compte de Alice : 1300
Les méthodes magiques : __str__ et __repr__
Les méthodes dunder (double underscore) définissent le comportement des objets dans des contextes particuliers.
__str__: représentation lisible pour les humains (utilisée parprint()etstr())__repr__: représentation technique pour les développeurs (utilisée dans le shell interactif)
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"Point({self.x}, {self.y})"
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
p = Point(3, 4)
print(p) # Point(3, 4) -- utilise __str__
print(repr(p)) # Point(x=3, y=4) -- utilise __repr__
print(str(p)) # Point(3, 4) -- utilise __str__
Autres méthodes magiques utiles
class Vecteur:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"({self.x}, {self.y})"
def __len__(self):
return 2 # Un vecteur 2D a toujours 2 composantes
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __add__(self, other):
return Vecteur(self.x + other.x, self.y + other.y)
v1 = Vecteur(1, 2)
v2 = Vecteur(3, 4)
v3 = v1 + v2
print(v3) # (4, 6)
print(len(v1)) # 2
print(v1 == v2) # False
print(v1 == Vecteur(1, 2)) # True
Héritage : class Enfant(Parent):
L'héritage permet à une classe enfant de réutiliser et d'étendre les fonctionnalités d'une classe parent.
class Animal:
def __init__(self, nom, age):
self.nom = nom
self.age = age
def respirer(self):
print(f"{self.nom} respire.")
def __str__(self):
return f"{self.nom} ({self.age} ans)"
class Chien(Animal): # Chien hérite de Animal
def __init__(self, nom, age, race):
super().__init__(nom, age) # Appel au constructeur du parent
self.race = race
def aboyer(self):
print(f"{self.nom} aboie : Woof !")
def __str__(self):
return f"{self.nom} - {self.race} ({self.age} ans)"
class Chat(Animal):
def __init__(self, nom, age, interieur=True):
super().__init__(nom, age)
self.interieur = interieur
def miauler(self):
print(f"{self.nom} miaule : Miaou !")
rex = Chien("Rex", 3, "Berger allemand")
felix = Chat("Felix", 5)
rex.respirer() # Rex respire. (méthode héritée)
rex.aboyer() # Rex aboie : Woof ! (méthode propre)
felix.miauler() # Felix miaule : Miaou !
print(rex) # Rex - Berger allemand (3 ans)
super().__init__() — appeler le constructeur parent
super() permet d'appeler une méthode de la classe parente. C'est essentiel dans __init__ pour initialiser correctement la partie héritée de l'objet.
class Employe(Personne):
def __init__(self, nom, age, poste, salaire):
super().__init__(nom, age) # Initialise nom et age via Personne.__init__
self.poste = poste
self.salaire = salaire
def augmenter(self, pourcentage):
self.salaire *= (1 + pourcentage / 100)
print(f"Nouveau salaire de {self.nom} : {self.salaire:.2f}")
Surcharge de méthodes (Method Overriding)
Une classe enfant peut redéfinir une méthode de la classe parente :
class Forme:
def aire(self):
return 0
def __str__(self):
return f"Forme avec aire = {self.aire()}"
class Rectangle(Forme):
def __init__(self, largeur, hauteur):
self.largeur = largeur
self.hauteur = hauteur
def aire(self): # Surcharge de la méthode parente
return self.largeur * self.hauteur
class Cercle(Forme):
def __init__(self, rayon):
self.rayon = rayon
def aire(self): # Surcharge de la méthode parente
import math
return math.pi * self.rayon ** 2
r = Rectangle(4, 5)
c = Cercle(3)
print(r) # Forme avec aire = 20
print(c) # Forme avec aire = 28.274333882308138
Héritage multiple (mention)
Python supporte l'héritage multiple (une classe peut hériter de plusieurs parents), mais c'est une fonctionnalité avancée à utiliser avec prudence :
class Volant:
def voler(self):
print("Je vole !")
class Nageant:
def nager(self):
print("Je nage !")
class Canard(Volant, Nageant):
def coin_coin(self):
print("Coin coin !")
donald = Canard()
donald.voler() # Je vole !
donald.nager() # Je nage !
donald.coin_coin()
isinstance() et issubclass()
rex = Chien("Rex", 3, "Labrador")
felix = Chat("Felix", 5)
print(isinstance(rex, Chien)) # True
print(isinstance(rex, Animal)) # True (Chien hérite de Animal)
print(isinstance(rex, Chat)) # False
print(isinstance(rex, object)) # True (tout est object en Python)
print(issubclass(Chien, Animal)) # True
print(issubclass(Chat, Chien)) # False
print(issubclass(Animal, object)) # True
Encapsulation : _name et __name
Python n'a pas de vraie visibilité privée comme Java ou PHP. Mais il existe des conventions :
class Voiture:
def __init__(self, marque, vitesse_max):
self.marque = marque # Public : accessible partout
self._kilometrage = 0 # Protected (convention) : usage interne
self.__code_secret = "ABC123" # Private (name mangling) : brouille le nom
def rouler(self, km):
self._kilometrage += km # Usage interne normal
def afficher_km(self):
return self._kilometrage
v = Voiture("Toyota", 180)
print(v.marque) # Toyota -- OK
print(v._kilometrage) # 0 -- fonctionne, mais déconseillé
# print(v.__code_secret) # AttributeError !
print(v._Voiture__code_secret) # ABC123 -- name mangling, accès possible mais très déconseillé
Le préfixe _ est une convention : "à usage interne, évitez d'y accéder directement".
Le préfixe __ applique le name mangling : Python renomme l'attribut en _Classe__nom.
Properties : @property et @setter
Les properties permettent de contrôler l'accès aux attributs avec des getters/setters :
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, valeur):
if valeur < -273.15:
raise ValueError("Temperature en dessous du zero absolu !")
self._celsius = valeur
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@property
def kelvin(self):
return self._celsius + 273.15
t = Temperature(100)
print(t.celsius) # 100
print(t.fahrenheit) # 212.0
print(t.kelvin) # 373.15
t.celsius = 0
print(t.fahrenheit) # 32.0
# t.celsius = -300 # ValueError !
Méthodes de classe et méthodes statiques
class Compteur:
total = 0 # Attribut de classe
def __init__(self, nom):
self.nom = nom
Compteur.total += 1
@classmethod
def creer_anonyme(cls):
"""Méthode de classe : crée une instance avec un nom par défaut."""
return cls(f"Anonyme-{cls.total + 1}")
@staticmethod
def convertir_en_romain(n):
"""Méthode statique : ne dépend ni de l'instance ni de la classe."""
if n == 1: return "I"
if n == 2: return "II"
if n == 3: return "III"
if n == 4: return "IV"
if n == 5: return "V"
return str(n)
c1 = Compteur("Alice")
c2 = Compteur.creer_anonyme() # Utilise la méthode de classe
print(Compteur.total) # 2
print(c2.nom) # Anonyme-2
print(Compteur.convertir_en_romain(4)) # IV
Exercices pratiques
Exercice 1 : Créer une classe avec __init__
Utilisez toujours le PascalCase pour les classes (Livre, CompteBancaire, VoitureElectrique). Le snake_case est réservé aux fonctions, méthodes et variables. Cette convention est définie dans PEP 8 et suivie par tout l'écosystème Python.
Exercice 2 : Ajouter une méthode à la classe
Ne jamais oublier self comme premier paramètre de toute méthode d'instance. Sans self, Python ne saura pas à quelle instance la méthode appartient et vous aurez une TypeError. self est une convention forte (on pourrait l'appeler autrement, mais ne le faites pas).
Exercice 3 : Ajouter la méthode __str__
Définissez toujours __str__ pour vos classes : c'est ce qui s'affiche avec print(). Pour le debug, ajoutez __repr__ qui doit donner une représentation que l'on pourrait coller dans Python pour recréer l'objet. Si vous ne définissez que __repr__, Python l'utilisera aussi pour str().
Exercice 4 : Créer une classe enfant avec héritage
Appelez toujours super().__init__() dans le constructeur d'une classe enfant pour initialiser correctement la partie héritée. Si vous oubliez, les attributs du parent ne seront pas initialisés et vous aurez des AttributeError difficiles à diagnostiquer.
Exercice 5 : Utiliser @property et @setter
Utilisez les properties pour ajouter de la validation ou du calcul autour des attributs sans changer l'API. Si un attribut est toujours simple, exposez-le directement. Si vous avez besoin de validation ou de calcul dérivé, refactorisez vers une property sans casser le code existant.
Exercice 6 : Surcharger une méthode dans la classe enfant
Quand vous surchargez une m éthode, assurez-vous que la méthode enfant reste cohérente avec le contrat de la méthode parente (même sémantique, mêmes types de retour). C'est le principe de substitution de Liskov (LSP) : partout où on attend un Animal, un Chien doit fonctionner sans surprise.