🧩

Programmation Orientée Objet

Classes · Héritage · Encapsulation · Polymorphisme

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) et super().__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.

Classe
Le moule / le plan
Défini une seule fois avec class
Objet / Instance
La pièce fabriquée
Créé autant de fois qu'on veut
Attribut
Donnée propre à l'objet
Accédé via objet.attribut
Méthode
Comportement de l'objet
Appelé via objet.methode()
💡
Convention de nommage : les classes s'écrivent en PascalCase (première lettre de chaque mot en majuscule) : 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
🚨
Piège : modifier un attribut de classe via une instance (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.

1
Nom imposé : __init__ — deux underscores avant ET après. Ni plus ni moins.
2
Premier paramètre toujours self — représente l'objet en cours de création. Python le passe automatiquement ; on ne l'écrit jamais lors de l'appel.
3
Déclarer chaque attribut avec self.nom = valeur — sans self., la variable est locale et disparaît à la fin du constructeur.
4
Jamais de 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

⚡ Piège A — Oublier les doubles underscores
✗ Faux
def init(self, nom):
    self.nom = nom
# Méthode ordinaire, pas le constructeur !
# L'objet sera créé mais vide.
✓ Correct
def __init__(self, nom):
    self.nom = nom
# __ avant ET __ après
⚡ Piège B — Oublier self. devant un attribut
✗ Faux — variable locale
def __init__(self, nom):
    nom = nom  # variable locale ! disparaît

e = Eleve("Alice")
print(e.nom)  # AttributeError !
✓ Correct
def __init__(self, nom):
    self.nom = nom  # attribut d'instance

e = Eleve("Alice")
print(e.nom)  # "Alice" ✓
⚡ Piège C — Mettre un return dans __init__
✗ Faux
def __init__(self, nom):
    self.nom = nom
    return self  # TypeError immédiat
✓ Correct
def __init__(self, nom):
    self.nom = nom
    # Pas de return — Python s'en charge
⚡ Piège D — Passer self à l'instanciation
✗ Faux
e = Eleve(self, "Alice")
# TypeError : trop d'arguments
✓ Correct
e = Eleve("Alice")
# self est passé automatiquement
⚡ Piège E — Appeler __init__ manuellement
✗ Faux
e = Eleve.__init__("Alice")
# On n'appelle jamais __init__ directement
✓ Correct
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

FamilleRôleUtilise return ?Exemple
Accesseur (getter)Lit et retourne une valeur sans modifier l'objet✅ Toujoursget_solde(), aire()
Mutateur (setter)Modifie un attribut❌ Jamaisdeposer(), 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

🚨
Piège classique : un attribut s'accède sans 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érationPython ✓Erreurs fréquentes ✗
ET logiqueandAND, &&
OU logiqueorOR, ||
NON logiquenotNOT, !
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 PythonSignificationAccès depuis l'extérieurExemple
attributPublic✅ Libreself.nom
_attributProtégé (convention)⚠️ Possible mais déconseilléself._vitesse
__attributPrivé (name mangling)❌ Inaccessible directementself.__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.

💡
Pour aller plus loin (hors programme bac) : Python dispose de méthodes spéciales comme __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
🚨
3 pièges des listes d'objets :
  • e.note() → TypeError — note est un attribut, pas une méthode
  • if e.est_admis (sans ()) → toujours True — on compare un objet méthode
  • return total dans 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.

✗ Sans super() — duplication
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
✓ Avec super() — délégation propre
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

SituationErreur fréquenteBonne pratique
Nom du constructeurdef init(self)def __init__(self)
Déclarer un attributnom = valeur (local)self.nom = valeur
Constructeurreturn self à la finPas de return
InstanciationClasse.__init__("x")Classe("x")
Lire un attributobjet.attribut()objet.attribut
Appeler une méthodeobjet.methodeobjet.methode()
Opérateur logiqueAND, &&and
Mutateurreturn self.attrPas de return
Méthode bool en boucleif e.est_admisif e.est_admis()
Chercher le maxInitialiser avec 0 ou NoneInitialiser avec liste[0]
return cumulatifReturn dans la boucleReturn après la boucle
Héritage — déclarerclass Enfant:class Enfant(Parent):
Héritage — init parentOublier super().__init__()super().__init__(...) en 1ère ligne
Étendre une méthodeRéécrire toute la logique parentsuper().methode() + ajouts

🏋️ Exercices

Intermédiaire Listes d'objets — Promotion d'élèves

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 appelant est_admis()
  • meilleur_eleve(promo) — retourne l'objet Eleve ayant 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
Niveau bac Héritage : Compte bancaire & Compte épargne

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 €
Niveau bac Polymorphisme : hiérarchie Pokémon

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__ = constructeur
  • self = 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
  • return après la boucle
  • methode() avec ()
  • liste[0] pour init du max

🧠 QCM

Choisissez le nombre de questions et le niveau, puis lancez.

← Graphes Bases de données →