34  POO (2/2) : héritage, encapsulation et méthodes spéciales

Le chapitre précédent a posé les bases : classes, instances, attributs, méthodes. Ce chapitre approfondit les 3 grands piliers qui rendent la POO puissante : l’héritage (réutiliser du code), l’encapsulation (protéger les données), et les méthodes spéciales (faire dialoguer nos objets avec Python).

34.1 L’héritage

L’héritage permet de créer une nouvelle classe (fille) qui réutilise et étend une classe existante (mère). C’est le mécanisme clé de la réutilisation en POO.

Exemple introductif

Imaginons qu’après avoir créé Personnage, on veuille une classe Guerrier — un personnage spécialisé. Sans héritage, on devrait tout redupliquer. Avec héritage :

# Classe mère
class Personnage:
    def __init__(self, nom, pv):
        self.nom = nom
        self.pv = pv

    def attaquer(self, cible):
        print(f"{self.nom} attaque {cible.nom}")

    def __str__(self):
        return f"{self.nom} ({self.pv} PV)"


# Classe fille : Guerrier EST UN Personnage
class Guerrier(Personnage):
    def __init__(self, nom, pv, arme):
        super().__init__(nom, pv)        # appelle le __init__ du parent
        self.arme = arme

    def charger(self, cible):
        print(f"{self.nom} charge {cible.nom} avec son {self.arme} !")


# Utilisation
aragorn = Guerrier("Aragorn", 100, "épée")
legolas = Personnage("Legolas", 80)

aragorn.attaquer(legolas)       # hérité du parent
aragorn.charger(legolas)        # spécifique à Guerrier
print(aragorn)                   # __str__ hérité
Aragorn attaque Legolas
Aragorn charge Legolas avec son épée !
Aragorn (100 PV)

Relation conceptuelle : Guerrier est un Personnage. On dit que Guerrier hérite de Personnage, ou qu’il en est une sous-classe.

Syntaxe

class ClasseFille(ClasseMere):
    ...

Dans le corps, on peut :

  1. Ajouter de nouveaux attributs et méthodes.
  2. Redéfinir (override) des méthodes existantes.
  3. Appeler les méthodes du parent avec super().

34.2 super() — accéder à la classe parente

super() permet d’appeler les méthodes du parent depuis la classe fille. Usage principal : dans __init__, pour initialiser d’abord ce que le parent fait, puis ajouter ses propres initialisations.

class Vehicule:
    def __init__(self, marque, annee):
        self.marque = marque
        self.annee = annee

    def presenter(self):
        print(f"Véhicule {self.marque}, {self.annee}")


class Voiture(Vehicule):
    def __init__(self, marque, annee, nb_portes):
        super().__init__(marque, annee)     # init du parent
        self.nb_portes = nb_portes           # spécifique à Voiture

    def presenter(self):
        super().presenter()                   # comportement parent
        print(f"  → {self.nb_portes} portes")   # spécifique ajouté


v = Voiture("Renault", 2022, 5)
v.presenter()
Véhicule Renault, 2022
  → 5 portes
Pourquoi super() plutôt que Vehicule.__init__(self, ...) ?

Les deux formes sont équivalentes ici, mais super() est préféré car :

  • Il gère correctement l’héritage multiple (plusieurs parents).
  • Il reste valide si on renomme la classe parente plus tard.
  • C’est l’idiome universellement reconnu.

Utilisez toujours super().

34.3 Redéfinir (override) une méthode

On peut remplacer une méthode du parent en la redéfinissant dans la fille.

class Animal:
    def cri(self):
        return "..."


class Chien(Animal):
    def cri(self):
        return "Wouf !"


class Chat(Animal):
    def cri(self):
        return "Miaou !"


class Canard(Animal):
    def cri(self):
        return "Coin coin"


# Polymorphisme : même appel, comportements différents
for animal in [Chien(), Chat(), Canard()]:
    print(f"{type(animal).__name__} : {animal.cri()}")
Chien : Wouf !
Chat : Miaou !
Canard : Coin coin

C’est ce qu’on appelle le polymorphisme : même interface (cri()), comportements différents selon le type.

Étendre une méthode (pas juste remplacer)

Parfois, on veut garder le comportement parent et ajouter quelque chose :

class Personnage:
    def se_presenter(self):
        print(f"Je suis {self.nom}")


class Heros(Personnage):
    def __init__(self, nom, pouvoir):
        self.nom = nom
        self.pouvoir = pouvoir

    def se_presenter(self):
        super().se_presenter()          # comportement parent d'abord
        print(f"  et j'ai le pouvoir : {self.pouvoir}")


h = Heros("Gandalf", "Magie")
h.se_presenter()
Je suis Gandalf
  et j'ai le pouvoir : Magie

34.4 Héritage multi-niveaux

On peut hériter d’une classe qui hérite elle-même d’une autre. La hiérarchie peut être profonde.

class Animal:
    def __init__(self, nom):
        self.nom = nom


class Mammifere(Animal):
    def __init__(self, nom, poils):
        super().__init__(nom)
        self.poils = poils


class Chien(Mammifere):
    def __init__(self, nom, race):
        super().__init__(nom, poils=True)
        self.race = race

    def aboyer(self):
        print(f"{self.nom} (race {self.race}) aboie !")


rex = Chien("Rex", "Berger")
rex.aboyer()
print(f"A des poils : {rex.poils}")
Rex (race Berger) aboie !
A des poils : True

isinstance et les hiérarchies

isinstance(obj, Classe) renvoie True pour toutes les classes parentes, pas juste la classe directe.

class Animal: pass
class Mammifere(Animal): pass
class Chien(Mammifere): pass

rex = Chien()

print(isinstance(rex, Chien))        # True
print(isinstance(rex, Mammifere))    # True (ancêtre)
print(isinstance(rex, Animal))       # True (ancêtre plus lointain)
True
True
True

C’est le comportement attendu : un Chien est un Mammifere, est un Animal.

34.5 L’encapsulation

L’encapsulation consiste à protéger les attributs d’un objet — éviter qu’ils soient modifiés n’importe comment depuis l’extérieur.

Le problème

Dans CompteBancaire du chapitre précédent, rien n’empêche :

class CompteBancaire:
    def __init__(self, titulaire, solde=0):
        self.titulaire = titulaire
        self.solde = solde

compte = CompteBancaire("Alice", 100)
compte.solde = 999_999       # ❌ triche
print(compte.solde)
999999

L’attribut solde devrait être modifié uniquement via deposer et retirer. Python propose des conventions pour signaler qu’un attribut est privé.

Convention : préfixe underscore _

Un attribut préfixé d’un underscore _ signale : « c’est privé, n’y touchez pas de l’extérieur ».

class CompteBancaire:
    def __init__(self, titulaire, solde=0):
        self.titulaire = titulaire
        self._solde = solde           # convention : privé

    def consulter_solde(self):
        return self._solde

    def deposer(self, montant):
        self._solde += montant

    def retirer(self, montant):
        if montant > self._solde:
            print("Fonds insuffisants")
            return
        self._solde -= montant


c = CompteBancaire("Alice", 100)
c.deposer(50)
print(c.consulter_solde())
# c._solde = 999999  # Techniquement possible, mais le _ dit "ne fais pas ça"
150
⚠️ « Privé » en Python est conventionnel, pas strict

Contrairement à Java ou C++, Python ne bloque pas l’accès à _solde. Le underscore est une convention sociale : « si tu y touches, tu prends la responsabilité ».

C’est la philosophie « we’re all consenting adults » de Python — on fait confiance au développeur.

Il existe un préfixe double underscore __ qui déclenche le name mangling (le nom est transformé en coulisses) mais son usage est rare et pointu. En pratique, on s’en tient à _.

34.6 Les @property — getters et setters à la Pythonique

Comment faire pour que compte.solde soit lisible comme un attribut mais que l’écriture soit contrôlée ? C’est le rôle du décorateur @property.

class CompteBancaire:
    def __init__(self, titulaire, solde=0):
        self.titulaire = titulaire
        self._solde = solde

    @property
    def solde(self):
        """Accès en lecture au solde."""
        return self._solde

    @solde.setter
    def solde(self, valeur):
        """Validation à l'écriture."""
        if valeur < 0:
            raise ValueError("Le solde ne peut pas être négatif")
        self._solde = valeur


c = CompteBancaire("Alice", 100)

# Lecture : comme un attribut classique
print(c.solde)

# Écriture : appelle le setter, qui valide
c.solde = 200
print(c.solde)
100
200
c = CompteBancaire("Alice", 100)
c.solde = -50    # déclenche la validation
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[10], line 2
      1 c = CompteBancaire("Alice", 100)
----> 2 c.solde = -50    # déclenche la validation

Cell In[9], line 15, in CompteBancaire.solde(self, valeur)
     11     @solde.setter
     12     def solde(self, valeur):
     13         """Validation à l'écriture."""
     14         if valeur < 0:
---> 15             raise ValueError("Le solde ne peut pas être négatif")
     16         self._solde = valeur

ValueError: Le solde ne peut pas être négatif

Avantages :

  • L’utilisateur écrit c.solde = 200 — syntaxe simple.
  • La classe garde la main sur les règles de validation.
  • On peut ajouter de la logique après coup sans changer l’API.

Propriété en lecture seule

Si on ne définit pas de setter, l’attribut devient lisible mais pas modifiable directement :

class Cercle:
    def __init__(self, rayon):
        self.rayon = rayon

    @property
    def aire(self):
        """Calculée à la demande à partir du rayon."""
        import math
        return math.pi * self.rayon ** 2


c = Cercle(5)
print(f"Aire : {c.aire:.2f}")
# c.aire = 99     # AttributeError : pas de setter
Aire : 78.54

34.7 Les méthodes spéciales (« dunders »)

Python possède un système unifié : toute opération de base (+, ==, len, print…) se traduit par un appel à une méthode spéciale préfixée et suffixée de doubles underscores : __add__, __eq__, __len__, __str__

On les appelle « dunders » (pour double underscore). En les implémentant, vous rendez vos objets compatibles avec le langage.

__str__ vs __repr__

Deux méthodes de représentation :

Méthode Usage Public cible
__str__ print(obj), str(obj) Utilisateur final (lisible)
__repr__ Affichage dans le REPL, repr(obj) Développeur (reproductible)
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __str__(self):
        return f"({self.x}, {self.y})"

    def __repr__(self):
        return f"Point({self.x}, {self.y})"


p = Point(3, 4)
print(p)          # utilise __str__
print(str(p))
print(repr(p))    # utilise __repr__
(3, 4)
(3, 4)
Point(3, 4)

Convention : __repr__ devrait renvoyer une chaîne qui, réévaluée, recrée l’objet. Ici, Point(3, 4) est du code Python valide.

__eq__ : comparaison ==

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __eq__(self, autre):
        return self.x == autre.x and self.y == autre.y


a = Point(3, 4)
b = Point(3, 4)
c = Point(1, 2)

print(a == b)      # True grâce à __eq__
print(a == c)      # False
True
False

Sans __eq__, a == b renverrait False (comparaison par identité d’objet, comme is).

__len__ : len(obj)

class Panier:
    def __init__(self):
        self.articles = []

    def ajouter(self, article):
        self.articles.append(article)

    def __len__(self):
        return len(self.articles)


p = Panier()
p.ajouter("pain")
p.ajouter("lait")
print(len(p))       # appelle __len__
2

__add__ : obj1 + obj2

class Vecteur:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __add__(self, autre):
        return Vecteur(self.x + autre.x, self.y + autre.y)

    def __str__(self):
        return f"({self.x}, {self.y})"


v1 = Vecteur(1, 2)
v2 = Vecteur(3, 4)
v3 = v1 + v2        # appelle __add__
print(v3)
(4, 6)

Tableau récapitulatif des principaux dunders

Dunder Déclenché par Usage
__init__ MaClasse(...) Initialisation
__str__ print(o), str(o) Représentation humaine
__repr__ REPL, repr(o) Représentation dév
__eq__ o1 == o2 Égalité
__lt__, __le__, __gt__, __ge__ <, <=, >, >= Comparaison
__len__ len(o) Taille
__contains__ x in o Appartenance
__add__ o1 + o2 Addition
__sub__, __mul__, __truediv__ -, *, / Autres opérateurs
__getitem__ o[i] Accès indexé
__setitem__ o[i] = v Modif indexée
__iter__, __next__ for x in o Itération (Partie 4)
__call__ o(...) Objet appelable

34.8 @dataclass — la classe en mode rapide (Python 3.7+)

Pour les classes qui sont surtout des conteneurs de données, Python propose le décorateur @dataclass. Il génère automatiquement __init__, __repr__, __eq__

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

p1 = Point(3, 4)
p2 = Point(3, 4)
p3 = Point(1, 2)

print(p1)             # __repr__ auto
print(p1 == p2)       # True (grâce à __eq__ auto)
print(p1 == p3)       # False
Point(x=3, y=4)
True
False

Sans @dataclass, il aurait fallu écrire :

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"
    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)

Options utiles

from dataclasses import dataclass, field

@dataclass
class Utilisateur:
    nom: str
    age: int = 0                           # avec valeur par défaut
    roles: list = field(default_factory=list)   # liste vide par instance

u = Utilisateur("Alice", 30)
u.roles.append("admin")
print(u)

u2 = Utilisateur("Bob")
print(u2)       # roles vide, pas partagé avec Alice
Utilisateur(nom='Alice', age=30, roles=['admin'])
Utilisateur(nom='Bob', age=0, roles=[])

field(default_factory=list) résout le piège du mutable par défaut — chaque instance a sa propre liste.

Quand utiliser @dataclass ?
  • ✅ Classes qui sont principalement des conteneurs de données (DTO, configurations, résultats…).
  • ✅ Quand on veut gagner du temps sur le boilerplate.
  • ❌ Classes avec beaucoup de logique métier : une classe classique est plus claire.

En pratique, @dataclass est très utilisé en data science et dans les API modernes.


🧩 Quiz 5.1 — POO avancée

Question 1

Quelle est la syntaxe correcte pour faire hériter B de A ?

  1. class B inherits A:
  2. class B extends A:
  3. class B(A):
  4. class B : A:

c) class B(A): — la classe mère est indiquée entre parenthèses après le nom de la classe fille.

Question 2

Que fait super().__init__(nom) dans une classe fille ?

  1. Crée un nouvel objet de la classe parente
  2. Appelle le __init__ de la classe parente
  3. Détruit l’objet courant
  4. Renvoie le nom de la classe parente

b) Appelle le __init__ de la classe parente — pour réutiliser l’initialisation du parent avant d’ajouter ses propres attributs.

Question 3

Que renvoie isinstance(rex, Animal) si rex est une instance de Chien, et Chien hérite de Mammifere qui hérite de Animal ?

  1. True
  2. False
  3. Une erreur
  4. None

a) Trueisinstance remonte toute la hiérarchie. Un Chien est aussi un Mammifere et un Animal.

Question 4

Quel est le sens de _solde en Python ?

  1. Un mot-clé réservé
  2. Un attribut protégé au niveau du langage (inaccessible depuis l’extérieur)
  3. Une convention indiquant un attribut privé (accès déconseillé)
  4. Une variable globale

c) Une convention — le _ est un marqueur social : « c’est privé, ne touchez pas ». Python n’empêche pas techniquement l’accès.

Question 5

Que permet @property ?

  1. Marquer un attribut comme obligatoire
  2. Permettre d’appeler une méthode sans parenthèses, comme un attribut
  3. Créer une méthode statique
  4. Supprimer un attribut

b) — une méthode décorée avec @property s’accède sans parenthèses : c.solde au lieu de c.solde(). Permet d’avoir l’apparence d’un attribut avec la logique d’une méthode.

Question 6

Quelle méthode spéciale est appelée quand on écrit print(obj) ?

  1. __print__
  2. __repr__
  3. __str__
  4. __display__

c) __str__print appelle str(obj), qui appelle __str__. Si __str__ n’est pas définie, Python utilise __repr__ en secours.

Question 7

Que fournit automatiquement @dataclass ?

  1. Un constructeur __init__
  2. Une représentation __repr__
  3. Une égalité __eq__
  4. Tout ce qui précède

d)@dataclass génère __init__, __repr__ et __eq__ automatiquement à partir des annotations de classe. Évite beaucoup de boilerplate.

Question 8

Que s’affiche-t-il ?

class A:
    def salut(self):
        print("A")

class B(A):
    def salut(self):
        super().salut()
        print("B")

B().salut()
  1. A
  2. B
  3. A puis B
  4. Une erreur

c) A puis BB.salut() appelle d’abord super().salut() (qui est A.salut), puis exécute son propre code.

class A:
    def salut(self):
        print("A")

class B(A):
    def salut(self):
        super().salut()
        print("B")

B().salut()
A
B

✏️ Exercice 5.1 — Héritage Animal / Chien / Chat

Créez :

  1. Une classe Animal avec nom, age, et une méthode se_presenter().
  2. Une classe Chien qui hérite de Animal, ajoute race, et ajoute aboyer().
  3. Une classe Chat qui hérite de Animal, ajoute couleur, et redéfinit se_presenter (pour mentionner la couleur).
  4. Créez quelques instances et testez.
class Animal:
    ...

class Chien(Animal):
    ...

class Chat(Animal):
    ...

# Tests
rex = Chien("Rex", 3, "Berger")
felix = Chat("Felix", 5, "noir")

rex.se_presenter()
rex.aboyer()
felix.se_presenter()
class Animal:
    def __init__(self, nom, age):
        self.nom = nom
        self.age = age

    def se_presenter(self):
        print(f"Je suis {self.nom}, j'ai {self.age} ans.")


class Chien(Animal):
    def __init__(self, nom, age, race):
        super().__init__(nom, age)
        self.race = race

    def aboyer(self):
        print(f"{self.nom} aboie : Wouf !")


class Chat(Animal):
    def __init__(self, nom, age, couleur):
        super().__init__(nom, age)
        self.couleur = couleur

    def se_presenter(self):
        super().se_presenter()              # comportement parent
        print(f"  Je suis {self.couleur}.")  # puis ajout spécifique


# Tests
rex = Chien("Rex", 3, "Berger")
felix = Chat("Felix", 5, "noir")

rex.se_presenter()
rex.aboyer()
print()
felix.se_presenter()

# isinstance à travers la hiérarchie
print(f"\nRex est un Animal ? {isinstance(rex, Animal)}")
print(f"Felix est un Chien ? {isinstance(felix, Chien)}")
Je suis Rex, j'ai 3 ans.
Rex aboie : Wouf !

Je suis Felix, j'ai 5 ans.
  Je suis noir.

Rex est un Animal ? True
Felix est un Chien ? False

✏️ Exercice 5.2 — Compte avec @property

Reprenez CompteBancaire et protégez l’attribut solde avec @property. Le setter doit refuser un solde négatif.

class CompteBancaire:
    ...

c = CompteBancaire("Alice", 100)
print(c.solde)
c.solde = 500
print(c.solde)
c.solde = -50      # doit lever une erreur
class CompteBancaire:
    def __init__(self, titulaire, solde=0):
        self.titulaire = titulaire
        self.solde = solde                  # passe par le setter

    @property
    def solde(self):
        return self._solde

    @solde.setter
    def solde(self, valeur):
        if valeur < 0:
            raise ValueError("Le solde ne peut pas être négatif")
        self._solde = valeur


c = CompteBancaire("Alice", 100)
print(c.solde)
c.solde = 500
print(c.solde)
100
500
c = CompteBancaire("Alice", 100)
c.solde = -50
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[21], line 2
      1 c = CompteBancaire("Alice", 100)
----> 2 c.solde = -50

Cell In[20], line 13, in CompteBancaire.solde(self, valeur)
     10     @solde.setter
     11     def solde(self, valeur):
     12         if valeur < 0:
---> 13             raise ValueError("Le solde ne peut pas être négatif")
     14         self._solde = valeur

ValueError: Le solde ne peut pas être négatif

Le setter est aussi appelé depuis __init__ quand on écrit self.solde = solde. Cela permet de valider dès la création (un CompteBancaire("Alice", -10) lèverait une erreur).


✏️ Exercice 5.3 — Classe Vecteur avec opérateurs

Créez une classe Vecteur 2D avec :

  • __init__(x, y)
  • __str__"(3, 4)"
  • __repr__"Vecteur(3, 4)"
  • __add__ : addition vectorielle (v1 + v2)
  • __eq__ : égalité
  • norme() : longueur du vecteur
class Vecteur:
    ...

v1 = Vecteur(3, 4)
v2 = Vecteur(1, 2)

print(v1)
print(repr(v1))
print(v1 + v2)
print(v1 == Vecteur(3, 4))
print(v1.norme())
from math import sqrt

class Vecteur:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f"({self.x}, {self.y})"

    def __repr__(self):
        return f"Vecteur({self.x}, {self.y})"

    def __add__(self, autre):
        return Vecteur(self.x + autre.x, self.y + autre.y)

    def __eq__(self, autre):
        return self.x == autre.x and self.y == autre.y

    def norme(self):
        return sqrt(self.x ** 2 + self.y ** 2)


# Tests
v1 = Vecteur(3, 4)
v2 = Vecteur(1, 2)

print(v1)
print(repr(v1))
print(v1 + v2)
print(v1 == Vecteur(3, 4))
print(v1 == v2)
print(f"Norme de {v1} : {v1.norme()}")
(3, 4)
Vecteur(3, 4)
(4, 6)
True
False
Norme de (3, 4) : 5.0
from math import sqrt

class Vecteur:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __str__(self):
        return f"({self.x}, {self.y})"

    def __add__(self, autre):
        return Vecteur(self.x + autre.x, self.y + autre.y)

    def __mul__(self, k):
        return Vecteur(self.x * k, self.y * k)


v = Vecteur(2, 3)
print(v * 5)
(10, 15)

✏️ Exercice 5.4 — @dataclass

Créez la classe Produit avec @dataclass ayant nom, prix, et quantite=1. Testez __init__, __repr__, __eq__ automatiques.

from dataclasses import dataclass

@dataclass
class Produit:
    ...

p1 = Produit("Pain", 1.20)
p2 = Produit("Pain", 1.20)
p3 = Produit("Lait", 0.95, 3)

print(p1)
print(p1 == p2)
print(p3)
from dataclasses import dataclass

@dataclass
class Produit:
    nom: str
    prix: float
    quantite: int = 1

    def valeur_totale(self):
        return self.prix * self.quantite


# Tests
p1 = Produit("Pain", 1.20)
p2 = Produit("Pain", 1.20)
p3 = Produit("Lait", 0.95, 3)

print(p1)              # __repr__ auto
print(p1 == p2)        # __eq__ auto : True
print(p3)
print(f"Valeur totale : {p3.valeur_totale():.2f} €")
Produit(nom='Pain', prix=1.2, quantite=1)
True
Produit(nom='Lait', prix=0.95, quantite=3)
Valeur totale : 2.85 €

Voici ce que @dataclass nous a évité d’écrire :

class Produit:
    def __init__(self, nom, prix, quantite=1):
        self.nom = nom
        self.prix = prix
        self.quantite = quantite

    def __repr__(self):
        return f"Produit(nom={self.nom!r}, prix={self.prix}, quantite={self.quantite})"

    def __eq__(self, other):
        if not isinstance(other, Produit):
            return NotImplemented
        return (self.nom, self.prix, self.quantite) == (other.nom, other.prix, other.quantite)

    def valeur_totale(self):
        return self.prix * self.quantite

Gain : 9 lignes économisées avec @dataclass, pour un résultat équivalent.


À retenir

Points clés du chapitre
  1. Héritage : class B(A):B est un A. Permet la réutilisation et le polymorphisme.
  2. super() : accéder aux méthodes du parent. Idiomatique pour chaîner les __init__.
  3. Redéfinir (override) une méthode : même nom dans la fille. On peut appeler super().methode() pour étendre.
  4. Encapsulation : convention _attribut pour « privé ». Python ne bloque pas, c’est social.
  5. @property : méthode qui s’accède comme un attribut, avec validation possible via @x.setter.
  6. Dunders (__str__, __eq__, __add__, __len__…) : rendent nos objets compatibles avec le langage.
  7. @dataclass : génère automatiquement __init__, __repr__, __eq__ pour les classes-conteneurs.
  8. isinstance remonte toute la hiérarchie d’héritage.

← Chapitre précédent : POO (1/2)Chapitre suivant : Gestion des exceptions →