La POO en Python
Qu'est-ce que la POO ?
La Programmation Orientee Objet (POO) est un paradigme de programmation qui organise le code autour d'objets plutot que de fonctions. Un objet regroupe des donnees (attributs) et des comportements (methodes) dans une seule entite.
Classes vs Objets
- Une classe est le moule, le plan de construction
- Un objet est une instance concrete creee a 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 Methodes
- Les attributs sont les donnees de l'objet (nom, age, couleur...)
- Les methodes sont les fonctions de l'objet (rouler, freiner, klaxonner...)
Definir une classe : class et PascalCase
En Python, les classes s'ecrivent avec le mot-cle 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 methode __init__ est le constructeur : elle est appelee automatiquement quand on cree un nouvel objet.
Le parametre self represente l'instance elle-meme. Il doit toujours etre le premier parametre de toute methode 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 : partage 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
Methodes d'instance
Les methodes d'instance prennent toujours self comme premier parametre. Elles peuvent acceder 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 methodes magiques : __str__ et __repr__
Les methodes dunder (double underscore) definissent le comportement des objets dans des contextes particuliers.
__str__: representaiton lisible pour les humains (utilisee parprint()etstr())__repr__: representaiton technique pour les developpeurs (utilisee 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 methodes 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
Heritage : class Enfant(Parent):
L'heritage permet a une classe enfant de reutiliser et d'etendre les fonctionnalites 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 herite 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. (methode heritee)
rex.aboyer() # Rex aboie : Woof ! (methode propre)
felix.miauler() # Felix miaule : Miaou !
print(rex) # Rex - Berger allemand (3 ans)
super().__init__() — appeler le constructeur parent
super() permet d'appeler une methode de la classe parente. C'est essentiel dans __init__ pour initialiser correctement la partie heritee 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 methodes (Method Overriding)
Une classe enfant peut redefinir une methode 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 methode parente
return self.largeur * self.hauteur
class Cercle(Forme):
def __init__(self, rayon):
self.rayon = rayon
def aire(self): # Surcharge de la methode 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
Heritage multiple (mention)
Python supporte l'heritage multiple (une classe peut heriter de plusieurs parents), mais c'est une fonctionnalite avancee a 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 herite 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 visibilite prive 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 deconseille
# print(v.__code_secret) # AttributeError !
print(v._Voiture__code_secret) # ABC123 -- name mangling, acces possible mais tres deconseille
Le prefixe _ est une convention : "a usage interne, evitez d'y acceder directement".
Le prefixe __ applique le name mangling : Python renomme l'attribut en _Classe__nom.
Properties : @property et @setter
Les properties permettent de controler l'acces 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 !
Methodes de classe et methodes statiques
class Compteur:
total = 0 # Attribut de classe
def __init__(self, nom):
self.nom = nom
Compteur.total += 1
@classmethod
def creer_anonyme(cls):
"""Methode de classe : cree une instance avec un nom par defaut."""
return cls(f"Anonyme-{cls.total + 1}")
@staticmethod
def convertir_en_romain(n):
"""Methode statique : ne depend 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 methode de classe
print(Compteur.total) # 2
print(c2.nom) # Anonyme-2
print(Compteur.convertir_en_romain(4)) # IV
Exercices pratiques
Exercice 1 : Creer une classe avec __init__
Utilisez toujours le PascalCase pour les classes (Livre, CompteBancaire, VoitureElectrique). Le snake_case est reserve aux fonctions, methodes et variables. Cette convention est definie dans PEP 8 et suivie par tout l'ecosysteme Python.
Exercice 2 : Ajouter une methode a la classe
Ne jamais oublier self comme premier parametre de toute methode d'instance. Sans self, Python ne saura pas a quelle instance la methode 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 methode __str__
Definissez toujours __str__ pour vos classes : c'est ce qui s'affiche avec print(). Pour le debug, ajoutez __repr__ qui doit donner une representation que l'on pourrait coller dans Python pour recreer l'objet. Si vous ne definissez que __repr__, Python l'utilisera aussi pour str().
Exercice 4 : Creer une classe enfant avec heritage
Appelez toujours super().__init__() dans le constructeur d'une classe enfant pour initialiser correctement la partie heritee. Si vous oubliez, les attributs du parent ne seront pas initialises et vous aurez des AttributeError difficiles a 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 derive, refactorisez vers une property sans casser le code existant.
Exercice 6 : Surcharger une methode dans la classe enfant
Quand vous surchargez une methode, assurez-vous que la methode enfant reste coherente avec le contrat de la methode parente (meme semantique, memes types de retour). C'est le principe de substitution de Liskov (LSP) : partout ou on attend un Animal, un Chien doit fonctionner sans surprise.
Quiz de revision
Une solution
Vous devez être connecté pour voir le contenu.