Aller au contenu principal

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 par print() et str())
  • __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__

Bonne pratique - Nommage des classes

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

Bonne pratique - self

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__

Bonne pratique - str vs repr

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

Bonne pratique - super().init()

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

Bonne pratique - Properties

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

Bonne pratique - Surcharge de méthodes

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.


Quiz de révision


Que fait __init__ dans une classe Python ?


Que représente 'self' dans une méthode d'instance ?


Quelle méthode faut-il définir pour que print(mon_objet) affiche quelque chose de lisible ?


Dans une classe enfant, comment appeler le constructeur de la classe parente ?


Quelle est la différence entre @classmethod et @staticmethod ?


📌 Une solution