2.3 · Programmation orientée objet (classes, héritage, dunder)

Niveau 2 · Structures de données & POO

2.3Programmation orientée objet (classes, héritage, dunder)

Objectif : Créer des classes, l'héritage et les méthodes spéciales (__init__, __str__…).
Temps estimé : 18 min · lecture autonome · Si vous n'avez que 5 min : lisez « L'essentiel en 30s » + le schéma.

L'essentiel en 30 secondes

  • La classe est le moule, l'objet la pièce : attributs (données) + méthodes (verbes sur self) voyagent ensemble — fini les dicts + fonctions dispersées.
  • Héritage = est-un : l'enfant spécialise le parent via super(), peu profond (≤ 3) — composition pour a-un.
  • Dunders = objets citoyens : __str__/__eq__/__len__ répondent à print/==/len — @dataclass pour les porte-données.

Mini-cas filé — Nabil modélise ses ventes en objets

Les dicts de Nabil se dispersent : des fonctions qui manipulent des ventes sans jamais vérifier les montants, des rapports qui dupliquent les mêmes calculs. Il modélise : une classe Vente (wilaya, montant, méthodes total et __str__ lisible), une classe RapportMensuel qui regroupe les ventes, puis une classe VenteRemise qui HÉRITE de Vente et ajoute un taux (super().__init__() réutilise le parent). Fini les dicts + fonctions éparpillées : données et comportements vivent ensemble. En 2.4, la création refusera les montants négatifs avec raise.

Suite du fil rouge 2.2 : les lignes de Nabil deviennent des objets — et continue en 2.4.

1. La classe : le moule, l'objet : la pièce

class Vente: + __init__(self, ...) définit le moule ; v = Vente("Alger", 1200) crée la pièce. self désigne l'objet lui-même dans ses méthodes : chaque objet accède à SES attributs (self.montant) indépendamment des autres. Les attributs sont les données, les méthodes sont les comportements (verbes sur self, jamais de global). Données + comportements voyagent ensemble : fini les dictionnaires + fonctions dispersées. Nabil crée ainsi deux ventes distinctes qui ne se mélangent jamais.

2. Héritage : spécialiser, pas copier

class VenteRemise(Vente): hérite et spécialise (taux, calcul remisé). super().__init__() réutilise le constructeur du parent au lieu de le recopier ; redéfinir une méthode dans l'enfant, c'est le polymorphisme. Règle : héritage pour du « est-un » (une VenteRemise est-une Vente), composition (« a-un » : un Rapport a-des Ventes) sinon. Trois niveaux max : au-delà, c'est un labyrinthe. Nabil ajoute ainsi les ventes remisées sans toucher au code existant.

3. Dunder : parler python couramment

Les méthodes dunder (double underscore) intègrent vos objets aux mécanismes natifs : __str__ pour un affichage lisible (print(v)), __eq__ pour l'égalité (==), __len__ pour la taille (len()). Sans elles, print(objet) affiche un cryptique <... object at 0x...>. Pour les porte-données simples, @dataclass génère __init__, __eq__ et __repr__ automatiquement. Les objets de Nabil répondent ainsi à print() et == comme les types natifs.

Erreur d'interprétation fréquente : « La POO, c'est juste des dicts avec des fonctions dedans — et self ne sert à rien ». Non. Un dict ne protège rien (clés libres, aucune validation, fonctions dispersées) ; une classe regroupe données + comportements, initialise en __init__ et isole chaque objet via self — deux objets ne se mélangent jamais. Oublier self, c'est partager ou perdre l'état. La règle de Nabil : données qui voyagent avec leurs règles = classe ; self écrit dans la définition, jamais à l'appel (Python le passe seul).
Objets propres Données et comportements ensemble Classe (moule)__init__ + self — attributs et méthodes+Héritage (est-un)super(), spécialiser, ≤ 3 niveaux+Dunder (__str__…)print, ==, len comme les natifs Héritage pour est-un, composition pour a-un.
Données et comportements ensemble
Version texte du schéma (SEO / lecteurs d'écran / traduction) : 1) Nabil définit le MOULE (class Vente + __init__) et crée des PIÈCES (objets) : self isole chaque objet, attributs + méthodes voyagent ensemble. 2) Il SPÉCIALISE par héritage (VenteRemise est-une Vente, super() réutilise le parent, ≤ 3 niveaux) et compose sinon (un Rapport a-des Ventes). 3) Il rend ses objets CITOYENS avec les dunders (__str__ pour print, __eq__ pour ==) et @dataclass pour les porte-données. Leçon : modéliser en objets au lieu de disperser dicts et fonctions.

🔎 Exemple concret — 🔎 Exemple concret — Les ventes de Nabil qui s'affichent

Objets lisibles et comparables, comme chez Nabil :

class Vente:
    def __init__(self, wilaya, montant):
        self.wilaya = wilaya
        self.montant = float(montant)
    def __str__(self):
        return f"{self.wilaya} : {self.montant:.2f} €"

class VenteRemise(Vente):
    def __init__(self, wilaya, montant, taux=0.1):
        super().__init__(wilaya, montant)
        self.taux = taux
    def net(self):
        return self.montant * (1 - self.taux)

v = VenteRemise("Alger", 1000)
print(v)          # Alger : 1000.00 €
print(f"Net : {v.net():.2f} €")  # Net : 900.00 €

Lecture : __init__ initialise, __str__ affiche, super() réutilise le parent.

Leçon : méthodes = verbes sur self, jamais de global.

Vocabulaire de la section

Classe / objet
La classe est le plan (moule) ; l'objet (instance) est une réalisation concrète créée à partir de ce plan : v = Vente(...).

Exemple simple : Nabil crée deux ventes distinctes depuis la même classe.

Ce que ce n'est pas : un dictionnaire libre sans règles ni comportements.

Réflexe : concept à modéliser = classe.

Attribut / méthode
L'attribut est une donnée de l'objet (self.montant) ; la méthode est une fonction attachée à l'objet, verbe sur self.

Exemple simple : La vente de Nabil porte wilaya et calcule net().

Ce que ce n'est pas : des fonctions globales dispersées loin des données.

Réflexe : verbe sur les données = méthode.

__init__ / self
__init__ est le constructeur, appelé à la création ; self désigne l'objet courant, écrit dans la définition mais passé automatiquement à l'appel.

Exemple simple : Vente("Alger", 1200) initialise self.wilaya et self.montant.

Ce que ce n'est pas : un paramètre à passer à la main à chaque appel.

Réflexe : créer un objet = __init__ via self.

Héritage
Une classe enfant hérite des attributs et méthodes d'un parent et les spécialise ; super() réutilise le parent. Réservé au « est-un », peu profond.

Exemple simple : VenteRemise hérite de Vente et ajoute le taux.

Ce que ce n'est pas : un copier-coller de la classe parent.

Réflexe : est-un = héritage ; a-un = composition.

Méthode dunder
Méthode à double underscore (__str__, __len__, __eq__) qui branche vos objets sur print(), ==, len() ; @dataclass les génère pour les porte-données.

Exemple simple : print(vente) affiche « Alger : 1000.00 € » grâce à __str__.

Ce que ce n'est pas : un affichage cryptique .

Réflexe : objet affichable/comparable = dunder.

En pratique — En pratique — Créer des classes et de l'héritage

  1. Définissez une classe Vente avec __init__(self, wilaya, montant) et une méthode afficher(). Créez deux objets distincts et vérifiez leur indépendance.
  2. Ajoutez __str__ pour un affichage lisible (print(v) → « Alger : 1000.00 € ») et __eq__ pour comparer deux ventes.
  3. Créez une classe VenteRemise qui HÉRITE de Vente, ajoute un taux et une méthode net(). Réutilisez le parent avec super().__init__().
  4. Redéfinissez net() dans l'enfant (polymorphisme) et testez @dataclass sur une classe porte-données simple.
Vous modélisez des concepts en classes, créez des objets, utilisez l'héritage et rendez vos objets lisibles avec __str__ : les fondations de la POO.

Script prêt à dire — défendre la classe devant l'équipe (30 s)

« Pour les ventes de Nabil, j'ai remplacé les dicts dispersés par une classe Vente : wilaya et montant initialisés en __init__, calculs en méthodes sur self — chaque objet est indépendant. Les remises héritent avec super(), et __str__ donne un affichage propre. On ne touche plus aux données sans passer par l'objet. »

À dire en montrant un dict bricolé puis le même cas en classe.

Modèle copier-coller — classe, héritage et dunder

class Vente:
    def __init__(self, wilaya, montant):
        if float(montant) < 0:
            raise ValueError("montant négatif interdit")
        self.wilaya = wilaya
        self.montant = float(montant)
    def __str__(self):
        return f"{self.wilaya} : {self.montant:.2f} €"
    def __eq__(self, autre):
        if not isinstance(autre, Vente):
            return NotImplemented
        return (self.wilaya, self.montant) == (autre.wilaya, autre.montant)

class VenteRemise(Vente):
    def __init__(self, wilaya, montant, taux=0.1):
        super().__init__(wilaya, montant)
        self.taux = taux
    def net(self):
        return self.montant * (1 - self.taux)

Vente simple

Une Vente de Nabil porte wilaya + montant et s'affiche en f-string via __str__.

Vente remisée

VenteRemise hérite et ajoute le taux : super() réutilise le parent, net() calcule.

Rapport mensuel

Le Rapport a-des Ventes (composition) : il totalise sans hériter — a-un, pas est-un.

Porte-données

Un récap simple devient @dataclass : __init__ et __eq__ générés, zéro code répétitif.

✓ Bonnes pratiques

  • Regrouper données + comportements en classe (jamais de dict + fonctions dispersées).
  • Hériter peu profond (≤ 3) pour est-un ; composer pour a-un ; super() pour réutiliser.
  • Ajouter __str__/__eq__ et @dataclass pour des objets citoyens.

✗ Pièges fréquents

  • Tout garder en dict + fonctions globales dispersées.
  • Utiliser l'héritage pour du « a-un » au lieu de la composition.
  • Empiler 6 niveaux d'héritage illisibles.

Points clés à retenir

  • La classe est le moule ; l'objet (instance) en est une réalisation.
  • __init__ initialise l'objet ; self désigne l'objet courant (passé automatiquement).
  • L'héritage permet à une classe enfant de réutiliser/étendre une classe parent (est-un, super()).
  • Les méthodes dunder (__str__…) intègrent vos objets aux mécanismes de Python ; @dataclass pour les porte-données.

Défi 48h — appliquez demain

Modélisez un vrai concept (celui de Nabil ou le vôtre) en classe avec __init__ + __str__, créez deux objets indépendants, ajoutez une classe enfant avec super() et testez l'égalité avec __eq__.

Checklist sortie de section
  • Je peux expliquer classe vs objet et le rôle de self.
  • Je sais créer une classe, une méthode et un héritage avec super().
  • J'ai rendu mes objets lisibles avec __str__ (et @dataclass si adapté).
Auto-évaluation 1-5 : est-ce que mes données voyagent avec leurs comportements ?est-ce que mon héritage reste peu profond et justifié ?

Questions fréquentes

À quoi sert self exactement ?

self représente l'objet sur lequel on appelle la méthode. Il permet à chaque objet d'accéder à SES propres attributs (self.montant) indépendamment des autres. Python le passe automatiquement à l'appel : vous l'écrivez dans la définition, pas à l'appel.

La POO est-elle indispensable pour débuter ?

Pour de petits scripts, non : les fonctions suffisent. Mais dès que les programmes grandissent — et pour comprendre les bibliothèques (pandas, Django…) qui l'utilisent partout — la POO devient essentielle. Investir ici paie sur le long terme.

Vérifiez votre compréhension

Que désigne « self » dans une méthode ?

Ressources supplémentaires (optionnel, pour aller plus loin)

Examen du niveau 2Questions inédites et durcies pour valider vos acquis

GUIDINEE.COM · Python