Programmation Orientée Objet
Classes · Héritage · Encapsulation · Polymorphisme
🎯 Introduction
La POO (Programmation Orientée Objet) est un paradigme qui organise le code autour d'objets — des entités qui combinent des données (attributs) et des comportements (méthodes). En programmation procédurale classique, les données et les fonctions sont séparées ; en POO, tout est regroupé de façon cohérente.
✅ Ce qu'il faut maîtriser pour le bac
- Créer une classe avec
__init__,self, attributs et méthodes - Distinguer attribut de classe et attribut d'instance
- Héritage :
class Enfant(Parent)etsuper().__init__() - Polymorphisme : même méthode, comportement différent selon le type réel
- Encapsulation : conventions
_attribut(protégé) et__attribut(privé) - Manipuler des listes d'objets : boucles, cumuls, filtres, recherche de max
🏗️ Classes et objets
Le moule et la pièce
Une classe est un plan, un moule. Un objet (ou instance) est une pièce concrète fabriquée à partir de ce moule. On peut créer autant d'instances que l'on veut à partir d'une seule classe.
classobjet.attributobjet.methode()Voiture, CompteBancaire, PokemonFeu. Les objets en minuscules_avec_underscore.
Anatomie d'une classe
class Voiture: ← 1. mot-clé class + PascalCase + deux-points
nb_roues = 4 ← 2. attribut de CLASSE (partagé par toutes les instances)
def __init__(self, marque, vitesse_max): ← 3. constructeur (toujours en premier)
self.marque = marque ← attribut d'INSTANCE (propre à chaque objet)
self.vitesse_max = vitesse_max
self._vitesse = 0 ← attribut "protégé" (convention _)
def accelerer(self, v): ← 4. méthodes (après le constructeur)
if self._vitesse + v <= self.vitesse_max:
self._vitesse += v
def vitesse(self):
return self._vitesse
def afficher(self):
print(self.marque, "à", self._vitesse, "km/h")
# Instanciation
ma_voiture = Voiture("Toyota", 180) # Python appelle __init__ automatiquement
ma_voiture.accelerer(80)
print(ma_voiture) # Toyota à 80 km/h
print(isinstance(ma_voiture, Voiture)) # True
print(Voiture.nb_roues) # 4 — attribut de classe
Attribut de classe vs attribut d'instance
Un attribut de classe est défini directement dans le corps de la classe (hors de toute méthode). Il est partagé par toutes les instances. Un attribut d'instance est défini dans __init__ via self. et est propre à chaque objet.
class Compteur:
total = 0 # attribut de CLASSE — partagé
def __init__(self, nom):
self.nom = nom # attribut d'INSTANCE — propre à chaque objet
Compteur.total += 1
c1 = Compteur("Alice")
c2 = Compteur("Bob")
print(Compteur.total) # 2 — les deux instances partagent cet attribut
print(c1.nom) # "Alice" — propre à c1
c1.total = 99) crée en fait un nouvel attribut d'instance qui masque l'attribut de classe. Pour modifier l'attribut de classe, utiliser Classe.attribut = valeur.
🔧 Le constructeur __init__
Rôle et syntaxe
Le constructeur est la méthode automatiquement appelée par Python lors de la création d'un objet. Son seul rôle : initialiser les attributs.
__init__ — deux underscores avant ET après. Ni plus ni moins.self — représente l'objet en cours de création. Python le passe automatiquement ; on ne l'écrit jamais lors de l'appel.self.nom = valeur — sans self., la variable est locale et disparaît à la fin du constructeur.return — le constructeur ne retourne rien. Python gère ça en interne.class Eleve:
def __init__(self, nom, note, absences=0): # absences vaut 0 par défaut
self.nom = nom
self.note = note
self.absences = absences
e1 = Eleve("Alice", 15) # absences = 0 (valeur par défaut)
e2 = Eleve("Bob", 11, 3) # absences = 3
print(e1.nom) # "Alice"
print(e2.note) # 11
Les 5 pièges du constructeur
def init(self, nom):
self.nom = nom
# Méthode ordinaire, pas le constructeur !
# L'objet sera créé mais vide.
def __init__(self, nom):
self.nom = nom
# __ avant ET __ après
self. devant un attributdef __init__(self, nom):
nom = nom # variable locale ! disparaît
e = Eleve("Alice")
print(e.nom) # AttributeError !
def __init__(self, nom):
self.nom = nom # attribut d'instance
e = Eleve("Alice")
print(e.nom) # "Alice" ✓
return dans __init__def __init__(self, nom):
self.nom = nom
return self # TypeError immédiat
def __init__(self, nom):
self.nom = nom
# Pas de return — Python s'en charge
self à l'instanciatione = Eleve(self, "Alice")
# TypeError : trop d'arguments
e = Eleve("Alice")
# self est passé automatiquement
__init__ manuellemente = Eleve.__init__("Alice")
# On n'appelle jamais __init__ directement
e = Eleve("Alice")
# NomClasse(args) → Python appelle __init__
Progression : du simple au complet
# Étape 1 — classe minimale
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
# Étape 2 — attribut calculé à la création
class Rectangle:
def __init__(self, l, h):
self.largeur = l
self.hauteur = h
self.aire = l * h # calculé à l'initialisation
# Étape 3 — valeur par défaut
class Compte:
def __init__(self, titulaire, solde=0):
self.titulaire = titulaire
self.solde = solde
c1 = Compte("Alice", 500) # solde = 500
c2 = Compte("Bob") # solde = 0 par défaut
⚙️ Les méthodes
Accesseurs et mutateurs
| Famille | Rôle | Utilise return ? | Exemple |
|---|---|---|---|
| Accesseur (getter) | Lit et retourne une valeur sans modifier l'objet | ✅ Toujours | get_solde(), aire() |
| Mutateur (setter) | Modifie un attribut | ❌ Jamais | deposer(), set_note() |
class Compte:
def __init__(self, titulaire, solde=0):
self.titulaire = titulaire
self.solde = solde
def get_solde(self): # accesseur — return obligatoire
return self.solde
def deposer(self, montant): # mutateur — pas de return
if montant > 0:
self.solde += montant
def retirer(self, montant):
if 0 < montant <= self.solde:
self.solde -= montant
def afficher(self):
print(self.titulaire, ":", self.solde, "€")
Attribut vs Méthode : les parenthèses
objet.attribut). Une méthode s'appelle avec parenthèses (objet.methode()). Sans (), on obtient l'objet méthode lui-même (toujours truthy) — bug silencieux redoutable.
r = Rectangle(4, 6)
# ✓ Correct
print(r.largeur) # attribut → pas de ()
print(r.aire()) # méthode → () obligatoires
# ✗ Bug silencieux
if r.est_grand: # compare l'OBJET méthode → toujours True !
...
Opérateurs logiques Python
| Opération | Python ✓ | Erreurs fréquentes ✗ |
|---|---|---|
| ET logique | and | AND, && |
| OU logique | or | OR, || |
| NON logique | not | NOT, ! |
| Supérieur ou égal | >= | => |
| Égalité | == | = (affectation !) |
def est_admis(self):
return self.note >= 10 and self.absences < 5
# and minuscule, >= sans espace entre les deux caractères
def mention(self):
if self.note >= 16: return "Très bien"
elif self.note >= 14: return "Bien"
elif self.note >= 12: return "Assez bien"
elif self.note >= 10: return "Passable"
else: return "Ajourné"
# Chaque elif hérite implicitement des conditions précédentes
🔒 Encapsulation
L'encapsulation consiste à regrouper données et comportements dans un objet, et à contrôler l'accès aux attributs via des méthodes dédiées (getters/setters) plutôt qu'un accès direct depuis l'extérieur.
| Convention Python | Signification | Accès depuis l'extérieur | Exemple |
|---|---|---|---|
attribut | Public | ✅ Libre | self.nom |
_attribut | Protégé (convention) | ⚠️ Possible mais déconseillé | self._vitesse |
__attribut | Privé (name mangling) | ❌ Inaccessible directement | self.__solde |
Getter / Setter
On contrôle l'accès aux attributs privés via des méthodes dédiées. Le getter retourne la valeur ; le setter valide avant de modifier.
class Batterie:
def __init__(self, charge):
self.__charge = charge # attribut privé
def get_charge(self): # getter — retourne
return self.__charge
def set_charge(self, val): # setter — valide puis modifie
if 0 <= val <= 100:
self.__charge = val # pas de return !
b = Batterie(50)
b.set_charge(80) # OK
b.set_charge(150) # ignoré : hors plage
print(b.get_charge()) # 80
✨ La méthode afficher
Au programme NSI Terminale, la seule méthode "spéciale" à maîtriser est __init__. Pour afficher les informations d'un objet, on définit une méthode ordinaire afficher(self) qui utilise print() directement.
__str__ (appelée par print(obj)), __eq__ (appelée par ==), __add__ (appelée par +)… Elles ne sont pas exigibles au baccalauréat mais peuvent apparaître dans un énoncé comme donnée à lire.
class Eleve:
def __init__(self, nom, note):
self.nom = nom
self.note = note
def afficher(self): # méthode ordinaire d'affichage
print(self.nom, ":", self.note)
e = Eleve("Alice", 15)
e.afficher() # Alice : 15
📋 Manipuler des listes d'objets
Un pattern très fréquent au baccalauréat : parcourir une liste d'instances pour cumuler, filtrer ou trouver un extremum.
# ── Créer une liste d'objets ──────────────────────────────────
promo = [Eleve("Alice", 15), Eleve("Bob", 9), Eleve("Clara", 12)]
# ── Cumuler un attribut ──────────────────────────────────────
def moyenne(promo):
if len(promo) == 0: return 0
total = 0
for e in promo:
total += e.note # attribut → PAS de ()
return total / len(promo) # return APRÈS la boucle
# ── Appeler une méthode en boucle ─────────────────────────────
def compter_admis(promo):
n = 0
for e in promo:
if e.est_admis(): # méthode → () OBLIGATOIRES
n += 1
return n
# ── Rechercher le maximum ─────────────────────────────────────
def meilleur(promo):
champion = promo[0] # initialiser avec le 1er OBJET (jamais 0 ou None)
for e in promo:
if e.note > champion.note:
champion = e
return champion # retourner l'OBJET entier, pas juste la note
# ── Construire une sous-liste filtrée ─────────────────────────
def admis(promo):
resultat = []
for e in promo:
if e.note >= 10:
resultat.append(e)
return resultat
e.note()→TypeError—noteest un attribut, pas une méthodeif e.est_admis(sans()) → toujoursTrue— on compare un objet méthodereturn totaldans la boucle → retourne seulement la première valeur
👨👩👧 Héritage & Polymorphisme
L'héritage permet à une classe fille de réutiliser et spécialiser le comportement d'une classe parente. La classe fille hérite automatiquement de tous les attributs et méthodes de la classe parente.
Syntaxe de base
class Animal:
def __init__(self, nom):
self.nom = nom
def parler(self):
return "..."
def afficher(self):
print(self.nom, "dit :", self.parler())
class Chien(Animal): # Chien hérite de Animal
def parler(self): # surcharge (override) de parler
return "Woof !"
class Chat(Animal):
def __init__(self, nom, interieur):
super().__init__(nom) # appelle le constructeur parent
self.interieur = interieur
def parler(self):
return "Miaou !"
# Polymorphisme : même interface, comportement différent selon le type réel
animaux = [Chien("Rex"), Chat("Felix", True)]
for a in animaux:
a.afficher() # appelle parler() → Chien puis Chat donnent des résultats différents
super() : déléguer au parent
super() donne accès aux méthodes de la classe parente. Usage principal : dans __init__ pour initialiser les attributs hérités sans les réécrire.
class CompteEpargne(Compte):
def __init__(self, titulaire, solde, taux):
# On réécrit ce que Compte fait déjà
self.titulaire = titulaire
self.solde = solde
self.taux = taux
class CompteEpargne(Compte):
def __init__(self, titulaire, solde, taux):
super().__init__(titulaire, solde)
self.taux = taux # spécifique à CompteEpargne
def capitaliser(self):
self.depot(self.solde() * self.taux)
On peut aussi utiliser super() pour étendre une méthode héritée (garder son comportement ET ajouter le sien) :
class PokemonElectrique(Pokemon):
def attaquer(self, cible):
super().attaquer(cible) # d'abord l'attaque normale
cible.paralysie = True # puis l'effet spécial
Polymorphisme
Le polymorphisme permet d'appeler la même méthode sur des objets de types différents, chacun répondant avec son propre comportement. Python détermine quelle version utiliser en regardant le type réel de l'objet (MRO).
class Pokemon:
def attaquer(self, cible): cible.pdv -= 10
class PokemonFeu(Pokemon):
def attaquer(self, cible): cible.pdv -= 15 # surcharge — plus puissant
class PokemonEau(Pokemon):
def attaquer(self, cible): cible.pdv -= 12
# Polymorphisme : même appel, comportements différents
equipe = [PokemonFeu("Salamèche", 39), PokemonEau("Carapuce", 44)]
cible = Pokemon("Pikachu", "Élec", 35)
for p in equipe:
p.attaquer(cible) # chaque p applique SA version de attaquer
isinstance()
isinstance(obj, Classe) retourne True si l'objet est une instance de la classe ou d'une de ses sous-classes.
s = PokemonFeu("Salamèche", 39)
isinstance(s, PokemonFeu) # True
isinstance(s, Pokemon) # True — un PokemonFeu EST AUSSI un Pokemon
isinstance(s, PokemonEau) # False
Carte des pièges POO — à mémoriser pour le bac
| Situation | Erreur fréquente | Bonne pratique |
|---|---|---|
| Nom du constructeur | def init(self) | def __init__(self) |
| Déclarer un attribut | nom = valeur (local) | self.nom = valeur |
| Constructeur | return self à la fin | Pas de return |
| Instanciation | Classe.__init__("x") | Classe("x") |
| Lire un attribut | objet.attribut() | objet.attribut |
| Appeler une méthode | objet.methode | objet.methode() |
| Opérateur logique | AND, && | and |
| Mutateur | return self.attr | Pas de return |
| Méthode bool en boucle | if e.est_admis | if e.est_admis() |
| Chercher le max | Initialiser avec 0 ou None | Initialiser avec liste[0] |
return cumulatif | Return dans la boucle | Return après la boucle |
| Héritage — déclarer | class Enfant: | class Enfant(Parent): |
| Héritage — init parent | Oublier super().__init__() | super().__init__(...) en 1ère ligne |
| Étendre une méthode | Réécrire toute la logique parent | super().methode() + ajouts |
🏋️ Exercices
La classe Eleve possède nom, note, absences et une méthode est_admis() (note ≥ 10 et absences < 5). Écrire :
moyenne(promo)— moyenne des notes (0 si liste vide)compter_admis(promo)— compte les élèves admis en appelantest_admis()meilleur_eleve(promo)— retourne l'objetEleveayant la note la plus haute
✅ Correction
class Eleve:
def __init__(self, nom, note, absences=0):
self.nom = nom; self.note = note; self.absences = absences
def est_admis(self):
return self.note >= 10 and self.absences < 5
def moyenne(promo):
if len(promo) == 0: return 0
total = 0
for e in promo: total += e.note
return total / len(promo)
def compter_admis(promo):
n = 0
for e in promo:
if e.est_admis(): n += 1 # () obligatoires !
return n
def meilleur_eleve(promo):
champion = promo[0] # 1er objet, pas 0 !
for e in promo:
if e.note > champion.note:
champion = e
return champion
Créer une classe Compte avec un attribut privé __solde, des méthodes solde(), depot(m) et retrait(m) (lève une erreur si solde insuffisant). Puis une classe CompteEpargne héritant de Compte avec un taux et une méthode capitaliser() qui ajoute les intérêts.
✅ Correction
class Compte:
def __init__(self, titulaire, solde=0):
self.titulaire = titulaire
self.__solde = solde # attribut privé
def solde(self): return self.__solde
def depot(self, m): self.__solde += m
def retrait(self, m):
if m > self.__solde: raise ValueError("Solde insuffisant")
self.__solde -= m
def afficher(self):
print(self.titulaire, ":", self.__solde, "€")
class CompteEpargne(Compte):
def __init__(self, titulaire, solde, taux):
super().__init__(titulaire, solde)
self.taux = taux
def capitaliser(self):
self.depot(self.solde() * self.taux)
ce = CompteEpargne("Alice", 1000, 0.03)
ce.capitaliser()
ce.afficher() # Alice : 1030.0 €
Créer une classe Pokemon avec nom, type_p, pdv, une méthode attaquer(cible) (−10 PV, min 0) et est_vaincu(). Créer les sous-classes PokemonFeu et PokemonEau avec leurs propres attaquer (dégâts différents). Écrire une fonction duel(p1, p2) qui simule un combat et retourne le gagnant.
✅ Correction
class Pokemon:
def __init__(self, nom, type_p, pdv):
self.nom = nom; self.type = type_p; self.pdv = pdv
def attaquer(self, cible):
cible.pdv = max(0, cible.pdv - 10)
def est_vaincu(self): return self.pdv <= 0
def afficher(self): print(self.nom, "[" + self.type + "]", self.pdv, "PV")
class PokemonFeu(Pokemon):
def __init__(self, nom, pdv):
super().__init__(nom, "Feu", pdv)
def attaquer(self, cible):
cible.pdv = max(0, cible.pdv - 15) # plus puissant
class PokemonEau(Pokemon):
def __init__(self, nom, pdv):
super().__init__(nom, "Eau", pdv)
def attaquer(self, cible):
cible.pdv = max(0, cible.pdv - 12)
def duel(p1, p2):
while not p1.est_vaincu() and not p2.est_vaincu():
p1.attaquer(p2)
if not p2.est_vaincu(): p2.attaquer(p1)
return p1 if not p1.est_vaincu() else p2
📌 Fiche synthèse
CLASSE
class Nom:— PascalCase__init__= constructeurself= référence à l'objet- Attribut de classe vs d'instance
isinstance(obj, Classe)
MÉTHODES
- Accesseur →
return - Mutateur → pas de
return and/or/not(minuscules)objet.attr≠objet.meth()
ENCAPSULATION
attr= public_attr= protégé (convention)__attr= privé (name mangling)- Getter/Setter pour les attributs privés
HÉRITAGE
class Fille(Parent):super().__init__()- Override : redéfinir méthode
- Polymorphisme : même nom, ≠ comportement
LISTES
- Accumulateur avant la boucle
returnaprès la bouclemethode()avec()liste[0]pour init du max
🧠 QCM
Choisissez le nombre de questions et le niveau, puis lancez.