Chapitre 10 — Programmation orientée objet : classes et objets¶
Durée estimée : 5 à 6 heures (cours + exercices + mini-projet)
Prérequis : Chapitre 09 — Modules et bibliothèque standard (et surtout les fonctions du chapitre 05 et les dictionnaires du chapitre 04).
Objectifs — À la fin de ce chapitre, tu sauras : - expliquer ce qu'est une classe, un objet (une instance), un attribut et une méthode ; - écrire une classe avec un constructeur
__init__et comprendre enfin ce qu'estself; - ajouter des méthodes qui lisent ou modifient l'état d'un objet ; - rendre tes objets lisibles avec__str__et__repr__; - distinguer attributs d'instance et attributs de classe, et éviter le piège de l'attribut mutable partagé ; - concevoir une petite classe propre : validation des données, exceptions, conventions de nommage.
Cours¶
1. Pourquoi des objets ?¶
Imagine que tu programmes un jeu avec des personnages. Avec ce que tu sais déjà, tu utiliserais sans doute des dictionnaires et des fonctions :
def creer_personnage(nom, points_de_vie):
return {"nom": nom, "pv": points_de_vie}
def subir_degats(personnage, degats):
personnage["pv"] = max(0, personnage["pv"] - degats)
heros = creer_personnage("Aria", 30)
subir_degats(heros, 12)
print(heros)
Ça marche. Mais quand le programme grandit, des problèmes apparaissent :
- les données (le dictionnaire) et les actions (les fonctions) sont séparées : rien n'indique que
subir_degatsest faite pour ces dictionnaires-là ; - une faute de frappe (
heros["vp"]) crée silencieusement une nouvelle clé ; - rien n'empêche de passer un dictionnaire de voiture à
subir_degats.
La programmation orientée objet (POO) propose de regrouper les données et les actions qui vont ensemble dans une même entité : un objet.
class Personnage:
def __init__(self, nom, points_de_vie):
self.nom = nom
self.pv = points_de_vie
def subir_degats(self, degats):
self.pv = max(0, self.pv - degats)
heros = Personnage("Aria", 30)
heros.subir_degats(12)
print(heros.nom, heros.pv)
Ne t'inquiète pas si la syntaxe te paraît étrange : tout ce chapitre sert à l'expliquer, ligne par ligne.
💡 Bonne nouvelle : tu utilises des objets depuis le début ! Une chaîne est un objet de la classe
str, avec ses méthodes.upper(),.split()… Une liste est un objet de la classelist, avec.append(),.sort()… Dans ce chapitre, tu apprends à créer tes propres types.
2. Classe et objet : le moule et les gâteaux¶
Deux mots à bien distinguer :
- Une classe est un plan de fabrication, un modèle. Elle décrit ce qu'auront tous les objets de ce type.
- Un objet (on dit aussi une instance) est un exemplaire concret, fabriqué à partir de ce plan.
💡 Analogie : la classe, c'est le moule à gâteaux ; les objets, ce sont les gâteaux. Un seul moule permet de fabriquer autant de gâteaux que tu veux. Ils ont tous la même forme, mais chacun peut avoir son propre parfum (ses propres valeurs). Manger un gâteau ne fait pas disparaître les autres.
Un objet possède :
- des attributs : ses données, ses caractéristiques (le nom, les points de vie…). Ce sont des variables attachées à l'objet.
- des méthodes : ses actions, ses comportements (subir des dégâts, se soigner…). Ce sont des fonctions attachées à l'objet.
Une astuce de conception : dans la description d'un problème, les noms communs deviennent souvent des classes ou des attributs, et les verbes des méthodes. « Un compte bancaire a un titulaire et un solde ; on peut déposer et retirer de l'argent. »
3. Ta première classe¶
La classe la plus simple possible :
class Chien:
pass
rex = Chien() # on crée un objet en « appelant » la classe
medor = Chien()
print(rex)
print(type(rex))
print(rex is medor)
(L'adresse 0x7f... change à chaque exécution : c'est l'emplacement de l'objet dans la mémoire.)
class Chien:définit la classe. Convention : un nom de classe s'écrit en CamelCase (Chien,CompteBancaire,PersonnageJoueur), alors que les variables et fonctions sont ensnake_case.Chien()crée un nouvel objet. On dit qu'on instancie la classe.rexetmedorsont deux objets distincts (isrenvoieFalse).
On peut techniquement ajouter des attributs à la main (rex.nom = "Rex"), mais ce n'est pas une bonne pratique : rien ne garantit que tous les chiens auront un nom. C'est le rôle du constructeur.
4. Le constructeur __init__ et les attributs¶
La méthode spéciale __init__ (deux tirets bas de chaque côté, on dit « dunder init », pour double underscore) est appelée automatiquement à la création de chaque objet. Elle sert à lui donner ses attributs de départ.
class Chien:
def __init__(self, nom, race, age):
self.nom = nom
self.race = race
self.age = age
rex = Chien("Rex", "berger allemand", 4)
medor = Chien("Médor", "labrador", 2)
print(rex.nom, rex.age)
print(medor.nom, medor.race)
Que se passe-t-il quand on écrit Chien("Rex", "berger allemand", 4) ?
- Python crée un objet
Chienvide. - Il appelle
__init__en lui passant cet objet tout neuf comme premier argument (self), suivi de tes arguments :nom="Rex",race="berger allemand",age=4. self.nom = nomcrée un attributnomsur l'objet, et y range la valeur du paramètrenom.- L'objet, maintenant rempli, est renvoyé et rangé dans
rex.
⚠️
self.nometnomsont deux choses différentes :nomest le paramètre (une variable locale qui disparaît à la fin de__init__),self.nomest l'attribut stocké dans l'objet (qui dure aussi longtemps que l'objet). On leur donne souvent le même nom par habitude, mais ce n'est pas obligatoire.
On accède aux attributs avec la notation pointée objet.attribut, et on peut les modifier :
class Chien:
def __init__(self, nom, race, age):
self.nom = nom
self.race = race
self.age = age
rex = Chien("Rex", "berger allemand", 4)
rex.age = 5 # modification
rex.age += 1 # encore une
print(rex.age)
print(vars(rex)) # tous les attributs, sous forme de dictionnaire
vars(objet) est très utile pour déboguer : il montre le contenu complet d'un objet.
Valeurs par défaut et attributs calculés¶
__init__ est une fonction comme les autres : ses paramètres peuvent avoir des valeurs par défaut (chapitre 05). Et tous les attributs ne viennent pas forcément d'un paramètre :
class Personnage:
def __init__(self, nom, classe="guerrier", niveau=1):
self.nom = nom
self.classe = classe
self.niveau = niveau
self.pv_max = 20 + niveau * 10 # calculé
self.pv = self.pv_max # au départ, en pleine forme
self.inventaire = [] # une liste vide, propre à CHAQUE personnage
aria = Personnage("Aria", "mage", niveau=3)
brutus = Personnage("Brutus")
print(aria.classe, aria.pv)
print(brutus.classe, brutus.pv)
aria.inventaire.append("bâton")
print(aria.inventaire, brutus.inventaire)
Chaque objet a sa propre liste inventaire, parce qu'elle est créée à l'intérieur de __init__, donc à nouveau pour chaque personnage. Retiens ce point : il est au cœur d'un piège classique (voir la section 8).
5. self, enfin expliqué¶
self est le mot qui intrigue le plus les débutants. C'est pourtant simple : self, c'est l'objet sur lequel on est en train de travailler.
Une classe est écrite une seule fois, mais sert pour des milliers d'objets. Quand le code de la classe dit « mon nom », il faut bien savoir de quel objet on parle. self est ce « moi ».
💡 Analogie : imagine une fiche de consignes distribuée à tous les élèves d'une classe : « Écris ton prénom en haut de ta feuille ». La fiche est la même pour tous, mais chaque élève l'applique à lui-même.
self, c'est le « ton / ta » de la fiche.
Voici le point clé. Quand tu écris :
Python le traduit automatiquement en :
L'objet placé avant le point devient le premier argument de la méthode. C'est pour ça que toute méthode doit déclarer self comme premier paramètre, même si on ne le passe jamais soi-même entre les parenthèses.
class Chien:
def __init__(self, nom):
self.nom = nom
def aboyer(self):
print(f"{self.nom} fait : Wouf !")
rex = Chien("Rex")
medor = Chien("Médor")
rex.aboyer() # self = rex
medor.aboyer() # self = medor
Chien.aboyer(rex) # exactement équivalent à rex.aboyer() (mais on ne l'écrit jamais ainsi)
self n'est pas un mot réservé : c'est une convention universelle. Techniquement, tu pourrais l'appeler moi, mais ne le fais pas, tout le monde s'attend à lire self.
6. Les méthodes¶
Une méthode est une fonction définie dans une classe. Elle a accès à l'objet via self, donc à tous ses attributs et à toutes ses autres méthodes.
On distingue en gros deux sortes de méthodes :
- celles qui modifient l'état de l'objet (ses attributs) ;
- celles qui calculent et renvoient une information, sans rien modifier.
class Personnage:
def __init__(self, nom, pv_max):
self.nom = nom
self.pv_max = pv_max
self.pv = pv_max
# --- méthodes qui modifient l'état ---
def subir_degats(self, degats):
self.pv = max(0, self.pv - degats)
def se_soigner(self, soin):
self.pv = min(self.pv_max, self.pv + soin)
# --- méthodes qui renvoient une information ---
def est_vivant(self):
return self.pv > 0
def barre_de_vie(self, largeur=10):
pleins = round(self.pv / self.pv_max * largeur)
return "[" + "█" * pleins + "·" * (largeur - pleins) + "]"
# --- une méthode qui en utilise d'autres ---
def afficher(self):
etat = "debout" if self.est_vivant() else "K.O."
print(f"{self.nom:<8} {self.barre_de_vie()} {self.pv:>3}/{self.pv_max} ({etat})")
aria = Personnage("Aria", 40)
aria.afficher()
aria.subir_degats(25)
aria.afficher()
aria.se_soigner(100)
aria.afficher()
aria.subir_degats(999)
aria.afficher()
Aria [██████████] 40/40 (debout)
Aria [████······] 15/40 (debout)
Aria [██████████] 40/40 (debout)
Aria [··········] 0/40 (K.O.)
Observe :
se_soignerne peut pas dépasserpv_maxgrâce àmin,subir_degatsne descend pas sous 0 grâce àmax. Les règles du jeu sont dans la classe, à un seul endroit : impossible de les oublier ailleurs dans le programme.afficherappelle d'autres méthodes du même objet avecself.est_vivant()etself.barre_de_vie(). Sansself., Python chercherait une fonction globaleest_vivantet provoquerait uneNameError.
Des objets qui interagissent¶
Une méthode peut recevoir un autre objet en paramètre. C'est là que la POO devient vraiment expressive :
class Personnage:
def __init__(self, nom, pv, force):
self.nom = nom
self.pv = pv
self.force = force
def est_vivant(self):
return self.pv > 0
def attaquer(self, cible):
cible.pv = max(0, cible.pv - self.force)
print(f"{self.nom} frappe {cible.nom} : -{self.force} PV (reste {cible.pv})")
heros = Personnage("Aria", 30, 7)
gobelin = Personnage("Gobelin", 18, 4)
while heros.est_vivant() and gobelin.est_vivant():
heros.attaquer(gobelin)
if gobelin.est_vivant():
gobelin.attaquer(heros)
gagnant = heros if heros.est_vivant() else gobelin
print(f"Vainqueur : {gagnant.nom}")
Aria frappe Gobelin : -7 PV (reste 11)
Gobelin frappe Aria : -4 PV (reste 26)
Aria frappe Gobelin : -7 PV (reste 4)
Gobelin frappe Aria : -4 PV (reste 22)
Aria frappe Gobelin : -7 PV (reste 0)
Vainqueur : Aria
Dans attaquer, self est l'attaquant et cible la victime : deux objets de la même classe, chacun avec ses propres attributs. Le code se lit presque comme une phrase : heros.attaquer(gobelin).
7. Rendre un objet lisible : __str__ et __repr__¶
Par défaut, afficher un objet donne un résultat peu parlant :
class Livre:
def __init__(self, titre, auteur, pages):
self.titre = titre
self.auteur = auteur
self.pages = pages
livre = Livre("Le Petit Prince", "Saint-Exupéry", 96)
print(livre)
On peut définir deux méthodes spéciales pour y remédier :
| Méthode | Appelée par | Destinée à | Style |
|---|---|---|---|
__str__ |
print(objet), str(objet), f-strings |
l'utilisateur | joli, lisible |
__repr__ |
repr(objet), la console interactive, l'affichage dans une liste |
le développeur | précis, sans ambiguïté, idéalement du code Python valide |
class Livre:
def __init__(self, titre, auteur, pages):
self.titre = titre
self.auteur = auteur
self.pages = pages
def __str__(self):
return f"« {self.titre} » de {self.auteur} ({self.pages} pages)"
def __repr__(self):
return f"Livre({self.titre!r}, {self.auteur!r}, {self.pages!r})"
livre = Livre("Le Petit Prince", "Saint-Exupéry", 96)
print(livre) # utilise __str__
print(f"Je lis {livre}") # __str__ aussi
print(repr(livre)) # __repr__
bibliotheque = [livre, Livre("Dune", "Herbert", 896)]
print(bibliotheque) # une liste affiche ses éléments avec __repr__ !
« Le Petit Prince » de Saint-Exupéry (96 pages)
Je lis « Le Petit Prince » de Saint-Exupéry (96 pages)
Livre('Le Petit Prince', 'Saint-Exupéry', 96)
[Livre('Le Petit Prince', 'Saint-Exupéry', 96), Livre('Dune', 'Herbert', 896)]
Règles pratiques :
__str__et__repr__doivent renvoyer une chaîne (return), pas l'afficher (print).- Si tu ne dois en écrire qu'une, écris
__repr__: quand__str__n'existe pas, Python utilise__repr__à sa place. - Le
!rdans la f-string appliquerepr()à la valeur : il ajoute les guillemets autour des chaînes, ce qui permet de recopier la sortie comme du code Python.
Ces méthodes à double tiret bas (__init__, __str__, __repr__…) s'appellent des méthodes spéciales (ou « méthodes magiques ») : on ne les appelle presque jamais directement, c'est Python qui les appelle pour nous au bon moment. Il en existe beaucoup d'autres, par exemple __eq__ pour == ou __len__ pour len(), que tu découvriras au fil du temps.
class Playlist:
def __init__(self, nom):
self.nom = nom
self.titres = []
def ajouter(self, titre):
self.titres.append(titre)
def __len__(self):
return len(self.titres)
ma_liste = Playlist("Running")
ma_liste.ajouter("Titre A")
ma_liste.ajouter("Titre B")
print(len(ma_liste))
8. Attributs de classe et attributs d'instance¶
Jusqu'ici, tous nos attributs étaient des attributs d'instance : créés avec self.xxx = ..., propres à chaque objet.
Un attribut de classe est défini directement dans le corps de la classe, en dehors de toute méthode. Il est partagé par tous les objets :
class Voiture:
nombre_de_roues = 4 # attribut de classe : commun à toutes les voitures
voitures_fabriquees = 0 # un compteur partagé
def __init__(self, marque, couleur):
self.marque = marque # attributs d'instance : propres à chaque voiture
self.couleur = couleur
Voiture.voitures_fabriquees += 1
v1 = Voiture("Renault", "rouge")
v2 = Voiture("Peugeot", "bleue")
print(v1.nombre_de_roues, v2.nombre_de_roues, Voiture.nombre_de_roues)
print(Voiture.voitures_fabriquees)
print(v1.couleur, v2.couleur)
Quand tu écris v1.nombre_de_roues, Python cherche d'abord un attribut d'instance nombre_de_roues sur v1 ; il n'en trouve pas, alors il regarde dans la classe. C'est pour ça que l'attribut de classe est visible depuis chaque objet.
⚠️ Pour modifier un attribut de classe, passe par le nom de la classe (
Voiture.voitures_fabriquees += 1). Si tu écrisself.voitures_fabriquees += 1, Python crée un nouvel attribut d'instance sur cet objet, qui masque celui de la classe, et le compteur partagé ne bouge pas.
Quand utiliser un attribut de classe ? Pour des constantes communes à tous les objets (TAUX_TVA = 0.2, PV_MAX = 100) ou des compteurs globaux. Pour tout le reste, utilise des attributs d'instance créés dans __init__.
Le piège de l'attribut de classe mutable¶
Ce qui suit est l'un des bugs les plus fréquents en POO Python :
class Panier:
articles = [] # ❌ une SEULE liste, partagée par tous les paniers !
def __init__(self, client):
self.client = client
def ajouter(self, article):
self.articles.append(article)
panier_alice = Panier("Alice")
panier_bob = Panier("Bob")
panier_alice.ajouter("pain")
print(panier_bob.articles) # Bob n'a rien acheté…
Le pain d'Alice apparaît dans le panier de Bob ! self.articles.append(...) ne crée pas d'attribut d'instance, il modifie la liste trouvée dans la classe, qui est commune à tous. ✅ Correction : crée la liste dans __init__, pour que chaque panier ait la sienne :
class Panier:
def __init__(self, client):
self.client = client
self.articles = [] # ✅ une nouvelle liste pour chaque panier
def ajouter(self, article):
self.articles.append(article)
panier_alice = Panier("Alice")
panier_bob = Panier("Bob")
panier_alice.ajouter("pain")
print(panier_alice.articles, panier_bob.articles)
9. Les objets sont des références¶
Comme les listes (chapitre 04), les objets sont manipulés par référence : une variable ne contient pas l'objet, elle pointe vers lui.
class Compteur:
def __init__(self):
self.valeur = 0
def incrementer(self):
self.valeur += 1
a = Compteur()
b = a # b pointe vers LE MÊME objet que a (pas une copie)
b.incrementer()
print(a.valeur)
print(a is b)
c = Compteur() # un nouvel objet
print(a is c)
💡 Analogie : une variable, c'est une étiquette collée sur un objet.
b = acolle une deuxième étiquette sur le même objet ; il n'y a toujours qu'un seul compteur.
Conséquence importante : quand tu passes un objet à une fonction, la fonction peut le modifier (c'est d'ailleurs ce que fait attaquer à la section 6 avec cible).
Et pour comparer deux objets ? Par défaut, == se comporte comme is : deux objets distincts ne sont jamais égaux, même avec les mêmes attributs. Tu peux changer cela en définissant __eq__ :
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, autre):
return self.x == autre.x and self.y == autre.y
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2, p1 is p2)
(Les dataclasses du chapitre 11 écriront __init__, __repr__ et __eq__ automatiquement à ta place.)
10. Protéger ses données : validation et encapsulation¶
Une classe est responsable de la cohérence de ses objets. Un compte bancaire ne devrait jamais avoir un dépôt négatif ; un personnage ne devrait jamais avoir plus de PV que son maximum. Le bon réflexe : valider dans les méthodes et lever une exception (chapitre 08) si la demande est invalide.
class Thermostat:
TEMPERATURE_MIN = 5
TEMPERATURE_MAX = 30
def __init__(self, temperature=19):
self._temperature = 0
self.regler(temperature) # on réutilise la validation dès la création
def regler(self, temperature):
if not self.TEMPERATURE_MIN <= temperature <= self.TEMPERATURE_MAX:
raise ValueError(
f"température hors limites : {temperature} "
f"(entre {self.TEMPERATURE_MIN} et {self.TEMPERATURE_MAX})"
)
self._temperature = temperature
def temperature(self):
return self._temperature
salon = Thermostat()
salon.regler(21)
print(salon.temperature())
try:
salon.regler(45)
except ValueError as erreur:
print("Refusé :", erreur)
print(salon.temperature())
La convention du tiret bas¶
Remarque le nom _temperature, avec un tiret bas au début. En Python, rien n'est vraiment « privé » : n'importe qui peut écrire salon._temperature = 99. Mais le tiret bas est une convention forte qui signifie :
« Attribut interne : ne le modifie pas directement depuis l'extérieur, passe par les méthodes prévues. »
C'est ce qu'on appelle l'encapsulation : l'objet expose une interface (ses méthodes publiques, comme regler et temperature) et cache ses détails internes. On en reparle au chapitre 11, où @property permettra d'écrire salon.temperature sans parenthèses tout en gardant la validation.
💡 Les exceptions personnalisées que tu as écrites au chapitre 08,
class SoldeInsuffisantError(Exception): pass, sont… des classes ! Le(Exception)entre parenthèses signifie que ta classe hérite deException: c'est le sujet du prochain chapitre.
11. Des objets qui contiennent des objets¶
Un attribut peut contenir n'importe quelle valeur : un nombre, une liste… ou d'autres objets. On construit ainsi des programmes entiers en assemblant des classes simples :
class Article:
def __init__(self, nom, prix, quantite=1):
self.nom = nom
self.prix = prix
self.quantite = quantite
def sous_total(self):
return self.prix * self.quantite
def __str__(self):
return f"{self.quantite} x {self.nom:<12} {self.sous_total():>7.2f} €"
class Commande:
def __init__(self, client):
self.client = client
self.articles = [] # une liste d'objets Article
def ajouter(self, article):
self.articles.append(article)
def total(self):
return sum(article.sous_total() for article in self.articles)
def afficher(self):
print(f"Commande de {self.client}")
for article in self.articles:
print(" ", article)
print(f" {'TOTAL':<16} {self.total():>7.2f} €")
commande = Commande("Léa")
commande.ajouter(Article("Cahier", 2.50, 3))
commande.ajouter(Article("Stylo", 1.20, 5))
commande.ajouter(Article("Cartable", 34.90))
commande.afficher()
Chaque classe a une responsabilité claire : Article sait calculer son sous-total et s'afficher ; Commande sait gérer une liste d'articles et calculer le total. Cette façon d'assembler des objets s'appelle la composition, approfondie au chapitre 11.
12. Ranger ses classes dans des modules¶
Une classe s'importe comme une fonction (chapitre 09). Une organisation courante : un module par classe importante, ou par groupe de classes liées.
# main.py
from commande import Article, Commande
panier = Commande("Hugo")
panier.ajouter(Article("Gomme", 0.80, 2))
panier.afficher()
13. Récapitulatif du vocabulaire¶
| Terme | Signification | Exemple |
|---|---|---|
| Classe | Le modèle, le plan | class Chien: |
| Objet / instance | Un exemplaire concret | rex = Chien("Rex") |
| Instancier | Créer un objet à partir d'une classe | Chien("Rex") |
| Attribut | Une donnée attachée à l'objet | rex.nom |
| Méthode | Une fonction attachée à l'objet | rex.aboyer() |
| Constructeur | La méthode qui initialise l'objet | __init__ |
self |
L'objet courant, à l'intérieur de la classe | self.nom = nom |
| Méthode spéciale | Méthode appelée par Python lui-même | __str__, __repr__, __len__ |
| Attribut de classe | Donnée partagée par toutes les instances | Voiture.nombre_de_roues |
Pièges fréquents¶
Piège 1 — Oublier self dans la définition d'une méthode¶
Le message semble absurde : « 0 argument attendu, mais 1 donné », alors qu'on n'a rien mis entre les parenthèses ! Souviens-toi : objet.methode() passe automatiquement l'objet en premier argument. ✅ Toute méthode commence par self : def aboyer(self):.
Piège 2 — Oublier self. devant un attribut¶
class Chien:
def __init__(self, nom):
self.nom = nom
def presenter(self):
print(f"Je suis {nom}")
Chien("Rex").presenter()
Dans presenter, il n'existe aucune variable locale nom : l'information est rangée dans l'objet. Python te souffle même la solution. ✅ print(f"Je suis {self.nom}").
Variante plus sournoise, sans message d'erreur immédiat :
class Chien:
def __init__(self, nom):
nom = nom # ❌ ne fait rien d'utile : affecte le paramètre à lui-même
print(Chien("Rex").nom)
✅ self.nom = nom.
Piège 3 — Mal écrire __init__¶
class Chien:
def _init_(self, nom): # un seul tiret bas de chaque côté !
self.nom = nom
Chien("Rex")
Python ne reconnaît pas _init_ comme constructeur : pour lui, la classe n'a pas de __init__, donc elle n'accepte aucun argument. Même symptôme avec __int__ ou __inti__. ✅ Deux tirets bas de chaque côté : __init__.
Piège 4 — L'attribut de classe mutable partagé¶
Déjà vu à la section 8, mais il mérite sa place ici tant il est fréquent :
class Panier:
articles = [] # ❌ partagé par tous les paniers
def ajouter(self, article):
self.articles.append(article)
✅ Toute liste, dictionnaire ou ensemble propre à chaque objet doit être créé dans __init__ : self.articles = [].
Même piège avec une valeur par défaut mutable dans __init__ (vu au chapitre 05) :
class Panier:
def __init__(self, client, articles=[]): # ❌ la liste par défaut est créée UNE fois
self.client = client
self.articles = articles
panier_alice = Panier("Alice")
panier_bob = Panier("Bob")
panier_alice.articles.append("lait")
print(panier_bob.articles)
✅ Utilise None comme valeur par défaut :
class Panier:
def __init__(self, client, articles=None):
self.client = client
self.articles = articles if articles is not None else []
Piège 5 — __str__ qui affiche au lieu de renvoyer¶
class Chien:
def __init__(self, nom):
self.nom = nom
def __str__(self):
print(f"Chien {self.nom}") # ❌ print au lieu de return
print(Chien("Rex"))
Chien Rex
Traceback (most recent call last):
...
TypeError: __str__ returned non-string (type NoneType)
✅ return f"Chien {self.nom}".
Piège 6 — Oublier les parenthèses en appelant une méthode¶
class Chien:
def __init__(self, nom):
self.nom = nom
def aboyer(self):
return "Wouf"
rex = Chien("Rex")
print(rex.aboyer)
Pas d'erreur, mais on affiche la méthode elle-même au lieu de l'appeler. Piège redoutable dans un if : if rex.est_vivant: est toujours vrai (une méthode n'est jamais « vide »). ✅ rex.aboyer().
Piège 7 — Appeler une méthode sur la classe au lieu d'un objet¶
Une méthode ordinaire a besoin d'un objet pour travailler. ✅ Crée d'abord un objet : Chien("Rex").aboyer(). (Le chapitre 11 présente les méthodes de classe et statiques, qui s'appellent sur la classe.)
Piège 8 — Oublier un argument du constructeur¶
Les arguments de Chien(...) sont transmis à __init__ (sauf self, fourni automatiquement). ✅ Chien("Rex"), ou une valeur par défaut dans __init__ si c'est pertinent.
Exercices¶
Exercice 1 ⭐ — Rectangle¶
Écris une classe Rectangle avec deux attributs largeur et hauteur, et les méthodes :
aire()etperimetre();est_carre()qui renvoieTruesi les deux côtés sont égaux ;__str__qui renvoie par exempleRectangle 4 x 3.
r = Rectangle(4, 3)
print(r) # Rectangle 4 x 3
print(r.aire(), r.perimetre()) # 12 14
print(r.est_carre()) # False
print(Rectangle(5, 5).est_carre()) # True
Exercice 2 ⭐ — Compteur de clics¶
Écris une classe Compteur avec :
- un attribut
valeurqui vaut 0 à la création (ou une valeur de départ optionnelle) ; incrementer(pas=1),decrementer(pas=1)(la valeur ne descend jamais sous 0) etreinitialiser();__repr__qui renvoieCompteur(valeur=...).
c = Compteur()
c.incrementer()
c.incrementer(5)
c.decrementer(2)
print(c) # Compteur(valeur=4)
c.decrementer(10)
print(c.valeur) # 0
c2 = Compteur(10)
print(c2) # Compteur(valeur=10)
Exercice 3 ⭐ — __str__ et __repr__¶
Écris une classe Film (titre, réalisateur, année, durée en minutes) avec :
__str__→Inception (2010), de Christopher Nolan — 2h28;__repr__→Film('Inception', 'Christopher Nolan', 2010, 148).
Crée une liste de trois films, affiche la liste entière avec print(films), puis chaque film avec une boucle. Explique la différence entre les deux affichages.
Exercice 4 ⭐⭐ — Le point qui se déplace¶
Écris une classe Point (coordonnées x et y) avec :
deplacer(dx, dy)qui modifie le point ;distance(autre)qui renvoie la distance avec un autrePoint(modulemath) ;milieu(autre)qui renvoie un nouveauPointsitué au milieu des deux ;__repr__→Point(3, 4);__eq__pour que deux points de mêmes coordonnées soient égaux.
a = Point(0, 0)
b = Point(3, 4)
print(a.distance(b)) # 5.0
print(a.milieu(b)) # Point(1.5, 2.0)
a.deplacer(3, 4)
print(a == b) # True
print(a is b) # False
Exercice 5 ⭐⭐ — Pile de tâches et attribut de classe¶
Écris une classe Tache avec un titre, une priorité (1 à 3) et un état terminee (False au départ). Chaque tâche reçoit automatiquement un numéro unique (1, 2, 3…), grâce à un attribut de classe qui compte les tâches créées.
terminer()passe la tâche à l'état terminé ;__str__→#1 [ ] Faire les courses (priorité 2)(ou[x]si terminée) ;- la priorité doit être entre 1 et 3, sinon
ValueError.
t1 = Tache("Faire les courses", 2)
t2 = Tache("Réviser Python", 1)
t2.terminer()
print(t1)
print(t2)
print(Tache.compteur)
Exercice 6 ⭐⭐ — Le tamagotchi¶
Écris une classe Tamagotchi avec un nom, une faim et un ennui, tous deux entre 0 (parfait) et 10 (critique), qui démarrent à 3. Méthodes :
nourrir(): faim − 4 ;jouer(): ennui − 4 (sans descendre sous 0) ;passer_le_temps(): faim + 2 et ennui + 1 (sans dépasser 10) ;humeur()qui renvoie"heureux"si la somme faim + ennui est ≤ 5,"grognon"si elle est ≤ 12,"malheureux"sinon ;__str__→Pixel : faim 3/10, ennui 3/10, humeur : grognon.
Écris un petit programme qui fait vivre l'animal sur une liste d'actions :
Pixel : faim 3/10, ennui 3/10, humeur : grognon
temps -> Pixel : faim 5/10, ennui 4/10, humeur : grognon
temps -> Pixel : faim 7/10, ennui 5/10, humeur : grognon
nourrir -> Pixel : faim 3/10, ennui 5/10, humeur : grognon
jouer -> Pixel : faim 3/10, ennui 1/10, humeur : heureux
temps -> Pixel : faim 5/10, ennui 2/10, humeur : grognon
temps -> Pixel : faim 7/10, ennui 3/10, humeur : grognon
temps -> Pixel : faim 9/10, ennui 4/10, humeur : malheureux
temps -> Pixel : faim 10/10, ennui 5/10, humeur : malheureux
Exercice 7 ⭐⭐⭐ — Carnet de contacts¶
Écris deux classes :
Contact: nom, téléphone, email (optionnel).__str__→Alice Martin — 06 12 34 56 78 — alice@mail.fr(sans la partie email si elle est absente).Carnet: contient une liste deContact. Méthodes :ajouter(contact): refuse un doublon de nom (insensible à la casse) en levant uneValueError;chercher(texte): renvoie la liste des contacts dont le nom contienttexte(insensible à la casse) ;supprimer(nom): supprime le contact, ou lève uneKeyErrors'il n'existe pas ;__len__;afficher(): affiche les contacts triés par nom.
carnet = Carnet()
carnet.ajouter(Contact("Alice Martin", "06 12 34 56 78", "alice@mail.fr"))
carnet.ajouter(Contact("Bob Durand", "07 98 76 54 32"))
carnet.ajouter(Contact("Alicia Keys", "06 00 00 00 00"))
print(len(carnet))
for contact in carnet.chercher("ali"):
print(contact)
carnet.supprimer("bob durand")
carnet.afficher()
3
Alice Martin — 06 12 34 56 78 — alice@mail.fr
Alicia Keys — 06 00 00 00 00
Alice Martin — 06 12 34 56 78 — alice@mail.fr
Alicia Keys — 06 00 00 00 00
Exercice 8 ⭐⭐⭐ — Sauvegarder des objets en JSON¶
Reprends la classe Tache de l'exercice 5 (sans le compteur automatique : le numéro est passé au constructeur) et ajoute :
- une méthode
vers_dict()qui renvoie un dictionnaire des attributs ; - une fonction (hors de la classe)
depuis_dict(donnees)qui crée uneTacheà partir d'un tel dictionnaire.
Puis écris sauvegarder(taches, chemin) et charger(chemin) qui enregistrent / relisent une liste de tâches dans un fichier JSON (chapitre 07). Vérifie qu'après un aller-retour, les tâches sont identiques.
taches = [Tache(1, "Courses", 2), Tache(2, "Sport", 3, terminee=True)]
sauvegarder(taches, "taches.json")
for tache in charger("taches.json"):
print(tache)
Mini-projet — Compte bancaire avec historique¶
Cahier des charges¶
Tu vas modéliser un petit système bancaire, dans un fichier banque.py.
Classe Operation : représente une opération passée.
- Attributs :
type_operation("dépôt","retrait","virement émis","virement reçu"),montant,date(undatetime),solde_apres,libelle(optionnel). __str__→14/03/2026 10:32 retrait -50.00 € solde : 150.00 € (Courses).
Classe CompteBancaire :
- Création avec un titulaire et un solde initial optionnel (0 par défaut). Chaque compte reçoit un numéro unique automatique, du type
FR-0001,FR-0002… (attribut de classe). deposer(montant, libelle="")etretirer(montant, libelle="").virer(montant, destinataire, libelle=""): transfère de l'argent vers un autre objetCompteBancaire. Le virement apparaît dans l'historique des deux comptes.- Un découvert autorisé (attribut, 0 par défaut) : un retrait ne peut pas faire descendre le solde en dessous de
-decouvert_autorise. - Validation :
- montant ≤ 0 →
MontantInvalideError; - solde insuffisant →
SoldeInsuffisantError(avec le solde disponible dans le message) ; - virement vers soi-même →
ValueError. Les deux exceptions personnalisées héritent d'une exceptionBanqueError(syntaxe vue au chapitre 08). - Le solde est stocké dans un attribut « interne »
_solde, consultable via une méthodesolde(). - L'historique est une liste d'
Operation. Méthodesafficher_releve()(avec le solde actuel) ettotal_par_type()qui renvoie un dictionnaire{type: total}. __str__→Compte FR-0001 (Alice) : 150.00 €,__repr__→CompteBancaire('Alice', solde=150.0).
Un programme de démonstration (sous if __name__ == "__main__":) crée deux comptes, effectue des opérations valides et invalides (interceptées proprement), puis affiche les relevés.
Étapes suggérées¶
- Écris les trois exceptions.
- Écris
Operationavec son__str__(teste-la seule, avec une date fixe). - Écris
CompteBancaireavec__init__,solde(),__str__,__repr__et le compteur de numéros. - Ajoute une méthode interne
_enregistrer(type_operation, montant, libelle)qui crée uneOperationet l'ajoute à l'historique : toutes les autres méthodes l'utiliseront. - Ajoute
deposer, puisretireravec le découvert, puisvireren réutilisantretireretdeposer… attention, les types d'opération doivent être « virement émis / reçu » : prévois un paramètre pour cela. - Ajoute
afficher_releveettotal_par_type(undefaultdictest idéal). - Écris la démonstration et teste toutes les erreurs.
Pistes d'amélioration¶
- Une classe
Banquequi contient tous les comptes, avec recherche par numéro et un menu interactif. - Sauvegarde et chargement des comptes en JSON (dates au format ISO).
- Filtrer le relevé par période ou par type d'opération.
- Calculer des intérêts mensuels sur un compte épargne (ce sera un bon exercice d'héritage au chapitre suivant !).
Corrigés¶
Corrigé de l'exercice 1
class Rectangle:
def __init__(self, largeur, hauteur):
self.largeur = largeur
self.hauteur = hauteur
def aire(self):
return self.largeur * self.hauteur
def perimetre(self):
return 2 * (self.largeur + self.hauteur)
def est_carre(self):
return self.largeur == self.hauteur
def __str__(self):
return f"Rectangle {self.largeur} x {self.hauteur}"
r = Rectangle(4, 3)
print(r)
print(r.aire(), r.perimetre())
print(r.est_carre())
print(Rectangle(5, 5).est_carre())
Corrigé de l'exercice 2
class Compteur:
def __init__(self, valeur=0):
self.valeur = valeur
def incrementer(self, pas=1):
self.valeur += pas
def decrementer(self, pas=1):
self.valeur = max(0, self.valeur - pas)
def reinitialiser(self):
self.valeur = 0
def __repr__(self):
return f"Compteur(valeur={self.valeur})"
c = Compteur()
c.incrementer()
c.incrementer(5)
c.decrementer(2)
print(c)
c.decrementer(10)
print(c.valeur)
c2 = Compteur(10)
print(c2)
Corrigé de l'exercice 3
class Film:
def __init__(self, titre, realisateur, annee, duree):
self.titre = titre
self.realisateur = realisateur
self.annee = annee
self.duree = duree
def __str__(self):
heures, minutes = divmod(self.duree, 60)
return f"{self.titre} ({self.annee}), de {self.realisateur} — {heures}h{minutes:02d}"
def __repr__(self):
return f"Film({self.titre!r}, {self.realisateur!r}, {self.annee}, {self.duree})"
films = [
Film("Inception", "Christopher Nolan", 2010, 148),
Film("Amélie", "Jean-Pierre Jeunet", 2001, 122),
Film("Le Voyage de Chihiro", "Hayao Miyazaki", 2001, 125),
]
print(films)
for film in films:
print(film)
[Film('Inception', 'Christopher Nolan', 2010, 148), Film('Amélie', 'Jean-Pierre Jeunet', 2001, 122), Film('Le Voyage de Chihiro', 'Hayao Miyazaki', 2001, 125)]
Inception (2010), de Christopher Nolan — 2h28
Amélie (2001), de Jean-Pierre Jeunet — 2h02
Le Voyage de Chihiro (2001), de Hayao Miyazaki — 2h05
Corrigé de l'exercice 4
import math
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def deplacer(self, dx, dy):
self.x += dx
self.y += dy
def distance(self, autre):
return math.dist((self.x, self.y), (autre.x, autre.y))
def milieu(self, autre):
return Point((self.x + autre.x) / 2, (self.y + autre.y) / 2)
def __eq__(self, autre):
return self.x == autre.x and self.y == autre.y
def __repr__(self):
return f"Point({self.x}, {self.y})"
a = Point(0, 0)
b = Point(3, 4)
print(a.distance(b))
print(a.milieu(b))
a.deplacer(3, 4)
print(a == b)
print(a is b)
Corrigé de l'exercice 5
class Tache:
compteur = 0 # attribut de classe : nombre de tâches créées
def __init__(self, titre, priorite):
if priorite not in (1, 2, 3):
raise ValueError(f"priorité invalide : {priorite} (1, 2 ou 3)")
Tache.compteur += 1
self.numero = Tache.compteur
self.titre = titre
self.priorite = priorite
self.terminee = False
def terminer(self):
self.terminee = True
def __str__(self):
case = "x" if self.terminee else " "
return f"#{self.numero} [{case}] {self.titre} (priorité {self.priorite})"
t1 = Tache("Faire les courses", 2)
t2 = Tache("Réviser Python", 1)
t2.terminer()
print(t1)
print(t2)
print(Tache.compteur)
try:
Tache("Tâche impossible", 7)
except ValueError as erreur:
print("Erreur :", erreur)
print(Tache.compteur)
Corrigé de l'exercice 6
class Tamagotchi:
MINIMUM = 0
MAXIMUM = 10
def __init__(self, nom):
self.nom = nom
self.faim = 3
self.ennui = 3
def _borner(self, valeur):
"""Ramène une valeur entre MINIMUM et MAXIMUM (méthode interne)."""
return max(self.MINIMUM, min(self.MAXIMUM, valeur))
def nourrir(self):
self.faim = self._borner(self.faim - 4)
def jouer(self):
self.ennui = self._borner(self.ennui - 4)
def passer_le_temps(self):
self.faim = self._borner(self.faim + 2)
self.ennui = self._borner(self.ennui + 1)
def humeur(self):
total = self.faim + self.ennui
if total <= 5:
return "heureux"
if total <= 12:
return "grognon"
return "malheureux"
def __str__(self):
return (f"{self.nom} : faim {self.faim}/10, ennui {self.ennui}/10, "
f"humeur : {self.humeur()}")
pixel = Tamagotchi("Pixel")
print(pixel)
actions = ["temps", "temps", "nourrir", "jouer", "temps", "temps", "temps", "temps"]
for action in actions:
match action:
case "nourrir":
pixel.nourrir()
case "jouer":
pixel.jouer()
case "temps":
pixel.passer_le_temps()
print(f"{action:<7} -> {pixel}")
Pixel : faim 3/10, ennui 3/10, humeur : grognon
temps -> Pixel : faim 5/10, ennui 4/10, humeur : grognon
temps -> Pixel : faim 7/10, ennui 5/10, humeur : grognon
nourrir -> Pixel : faim 3/10, ennui 5/10, humeur : grognon
jouer -> Pixel : faim 3/10, ennui 1/10, humeur : heureux
temps -> Pixel : faim 5/10, ennui 2/10, humeur : grognon
temps -> Pixel : faim 7/10, ennui 3/10, humeur : grognon
temps -> Pixel : faim 9/10, ennui 4/10, humeur : malheureux
temps -> Pixel : faim 10/10, ennui 5/10, humeur : malheureux
Corrigé de l'exercice 7
class Contact:
def __init__(self, nom, telephone, email=""):
self.nom = nom
self.telephone = telephone
self.email = email
def __str__(self):
texte = f"{self.nom} — {self.telephone}"
if self.email:
texte += f" — {self.email}"
return texte
class Carnet:
def __init__(self):
self.contacts = []
def _trouver(self, nom):
"""Renvoie le contact portant ce nom (casse ignorée), ou None."""
for contact in self.contacts:
if contact.nom.lower() == nom.lower():
return contact
return None
def ajouter(self, contact):
if self._trouver(contact.nom) is not None:
raise ValueError(f"le contact {contact.nom!r} existe déjà")
self.contacts.append(contact)
def chercher(self, texte):
texte = texte.lower()
return [contact for contact in self.contacts if texte in contact.nom.lower()]
def supprimer(self, nom):
contact = self._trouver(nom)
if contact is None:
raise KeyError(nom)
self.contacts.remove(contact)
def __len__(self):
return len(self.contacts)
def afficher(self):
for contact in sorted(self.contacts, key=lambda c: c.nom.lower()):
print(contact)
carnet = Carnet()
carnet.ajouter(Contact("Alice Martin", "06 12 34 56 78", "alice@mail.fr"))
carnet.ajouter(Contact("Bob Durand", "07 98 76 54 32"))
carnet.ajouter(Contact("Alicia Keys", "06 00 00 00 00"))
print(len(carnet))
for contact in carnet.chercher("ali"):
print(contact)
carnet.supprimer("bob durand")
carnet.afficher()
try:
carnet.ajouter(Contact("ALICE MARTIN", "01 01 01 01 01"))
except ValueError as erreur:
print("Refusé :", erreur)
try:
carnet.supprimer("Zoé")
except KeyError as erreur:
print("Introuvable :", erreur)
Corrigé de l'exercice 8
import json
from pathlib import Path
class Tache:
def __init__(self, numero, titre, priorite, terminee=False):
self.numero = numero
self.titre = titre
self.priorite = priorite
self.terminee = terminee
def vers_dict(self):
return {
"numero": self.numero,
"titre": self.titre,
"priorite": self.priorite,
"terminee": self.terminee,
}
def __str__(self):
case = "x" if self.terminee else " "
return f"#{self.numero} [{case}] {self.titre} (priorité {self.priorite})"
def depuis_dict(donnees):
return Tache(donnees["numero"], donnees["titre"], donnees["priorite"], donnees["terminee"])
def sauvegarder(taches, chemin):
donnees = [tache.vers_dict() for tache in taches]
Path(chemin).write_text(json.dumps(donnees, indent=2, ensure_ascii=False), encoding="utf-8")
def charger(chemin):
donnees = json.loads(Path(chemin).read_text(encoding="utf-8"))
return [depuis_dict(element) for element in donnees]
taches = [Tache(1, "Courses", 2), Tache(2, "Sport", 3, terminee=True)]
sauvegarder(taches, "taches.json")
relues = charger("taches.json")
for tache in relues:
print(tache)
assert [t.vers_dict() for t in relues] == [t.vers_dict() for t in taches]
print(Path("taches.json").read_text(encoding="utf-8"))
Corrigé du mini-projet
"""Comptes bancaires avec historique des opérations."""
from collections import defaultdict
from datetime import datetime
class BanqueError(Exception):
pass
class MontantInvalideError(BanqueError):
pass
class SoldeInsuffisantError(BanqueError):
pass
class Operation:
def __init__(self, type_operation, montant, solde_apres, libelle="", date=None):
self.type_operation = type_operation
self.montant = montant # positif pour un crédit, négatif pour un débit
self.solde_apres = solde_apres
self.libelle = libelle
self.date = date if date is not None else datetime.now()
def __str__(self):
texte = (f"{self.date:%d/%m/%Y %H:%M} {self.type_operation:<15} "
f"{self.montant:>+9.2f} € solde : {self.solde_apres:>8.2f} €")
if self.libelle:
texte += f" ({self.libelle})"
return texte
class CompteBancaire:
_dernier_numero = 0 # attribut de classe : compteur partagé
def __init__(self, titulaire, solde_initial=0.0, decouvert_autorise=0.0):
CompteBancaire._dernier_numero += 1
self.numero = f"FR-{CompteBancaire._dernier_numero:04d}"
self.titulaire = titulaire
self.decouvert_autorise = decouvert_autorise
self._solde = 0.0
self.historique = []
if solde_initial > 0:
self.deposer(solde_initial, "Dépôt initial")
# ---------- consultation ----------
def solde(self):
return self._solde
def __str__(self):
return f"Compte {self.numero} ({self.titulaire}) : {self._solde:.2f} €"
def __repr__(self):
return f"CompteBancaire({self.titulaire!r}, solde={self._solde!r})"
# ---------- méthodes internes ----------
@staticmethod
def _verifier_montant(montant):
if montant <= 0:
raise MontantInvalideError(f"le montant doit être positif (reçu : {montant})")
def _enregistrer(self, type_operation, montant, libelle):
self._solde += montant
self.historique.append(Operation(type_operation, montant, self._solde, libelle))
# ---------- opérations ----------
def deposer(self, montant, libelle="", type_operation="dépôt"):
self._verifier_montant(montant)
self._enregistrer(type_operation, montant, libelle)
def retirer(self, montant, libelle="", type_operation="retrait"):
self._verifier_montant(montant)
disponible = self._solde + self.decouvert_autorise
if montant > disponible:
raise SoldeInsuffisantError(
f"retrait de {montant:.2f} € refusé, disponible : {disponible:.2f} €"
)
self._enregistrer(type_operation, -montant, libelle)
def virer(self, montant, destinataire, libelle=""):
if destinataire is self:
raise ValueError("impossible de faire un virement vers le même compte")
# retirer d'abord : s'il échoue, rien n'est déposé chez le destinataire
self.retirer(montant, f"vers {destinataire.titulaire} {libelle}".strip(),
type_operation="virement émis")
destinataire.deposer(montant, f"de {self.titulaire} {libelle}".strip(),
type_operation="virement reçu")
# ---------- rapports ----------
def total_par_type(self):
totaux = defaultdict(float)
for operation in self.historique:
totaux[operation.type_operation] += operation.montant
return dict(totaux)
def afficher_releve(self):
print(f"=== Relevé du compte {self.numero} — {self.titulaire} ===")
if not self.historique:
print("(aucune opération)")
for operation in self.historique:
print(operation)
print(f"Solde actuel : {self._solde:.2f} €")
print()
if __name__ == "__main__":
alice = CompteBancaire("Alice", 200)
bob = CompteBancaire("Bob", decouvert_autorise=100)
print(alice)
print(bob)
print(repr(alice))
print()
operations = [
("Alice retire 50 €", lambda: alice.retirer(50, "Courses")),
("Bob retire 80 € (découvert)", lambda: bob.retirer(80, "Loyer")),
("Bob retire 50 € de plus", lambda: bob.retirer(50)),
("Alice dépose -10 €", lambda: alice.deposer(-10)),
("Alice vire 120 € à Bob", lambda: alice.virer(120, bob, "Remboursement")),
("Alice vire 500 € à Bob", lambda: alice.virer(500, bob)),
("Alice se vire 10 €", lambda: alice.virer(10, alice)),
]
for description, action in operations:
try:
action()
except BanqueError as erreur:
print(f"✗ {description} : {type(erreur).__name__} — {erreur}")
except ValueError as erreur:
print(f"✗ {description} : {erreur}")
else:
print(f"✓ {description}")
print()
alice.afficher_releve()
bob.afficher_releve()
print("Totaux d'Alice :", alice.total_par_type())
Compte FR-0001 (Alice) : 200.00 €
Compte FR-0002 (Bob) : 0.00 €
CompteBancaire('Alice', solde=200.0)
✓ Alice retire 50 €
✓ Bob retire 80 € (découvert)
✗ Bob retire 50 € de plus : SoldeInsuffisantError — retrait de 50.00 € refusé, disponible : 20.00 €
✗ Alice dépose -10 € : MontantInvalideError — le montant doit être positif (reçu : -10)
✓ Alice vire 120 € à Bob
✗ Alice vire 500 € à Bob : SoldeInsuffisantError — retrait de 500.00 € refusé, disponible : 30.00 €
✗ Alice se vire 10 € : impossible de faire un virement vers le même compte
=== Relevé du compte FR-0001 — Alice ===
14/03/2026 10:32 dépôt +200.00 € solde : 200.00 € (Dépôt initial)
14/03/2026 10:32 retrait -50.00 € solde : 150.00 € (Courses)
14/03/2026 10:32 virement émis -120.00 € solde : 30.00 € (vers Bob Remboursement)
Solde actuel : 30.00 €
=== Relevé du compte FR-0002 — Bob ===
14/03/2026 10:32 retrait -80.00 € solde : -80.00 € (Loyer)
14/03/2026 10:32 virement reçu +120.00 € solde : 40.00 € (de Alice Remboursement)
Solde actuel : 40.00 €
Totaux d'Alice : {'dépôt': 200.0, 'retrait': -50.0, 'virement émis': -120.0}
Récapitulatif¶
- Une classe est un modèle (le moule) ; un objet ou instance est un exemplaire concret (le gâteau). On crée un objet en appelant la classe :
Chien("Rex"). - Les attributs sont les données d'un objet (
self.nom), les méthodes ses actions (def aboyer(self):). __init__est le constructeur : il est appelé automatiquement à la création et initialise les attributs.selfest l'objet courant.rex.aboyer()équivaut àChien.aboyer(rex): toute méthode reçoitselfen premier paramètre, et on accède aux attributs avecself..__str__(affichage pour l'utilisateur) et__repr__(affichage pour le développeur) doivent renvoyer une chaîne.- Un attribut de classe est partagé par toutes les instances : réservé aux constantes et compteurs. Les listes et dictionnaires propres à chaque objet se créent dans
__init__. - Les objets sont manipulés par référence :
b = ane copie pas l'objet.isteste l'identité,==l'égalité (personnalisable avec__eq__). - Une classe protège sa cohérence en validant dans ses méthodes et en levant des exceptions. Le tiret bas (
_solde) signale un attribut interne. - Les classes se combinent : un objet peut contenir d'autres objets (composition).
- Conventions :
CamelCasepour les classes,snake_casepour les attributs et méthodes.
Checklist de validation¶
- Je sais expliquer la différence entre une classe et un objet avec mes propres mots.
- Je sais écrire une classe avec un
__init__et plusieurs attributs. - Je sais expliquer ce qu'est
selfet pourquoiobjet.methode()passe l'objet automatiquement. - Je sais écrire des méthodes qui modifient l'objet et d'autres qui renvoient une valeur.
- Mes classes ont un
__repr__(et un__str__si besoin) qui renvoie une chaîne. - Je sais pourquoi
articles = []au niveau de la classe est un bug. - Je sais faire interagir deux objets (une méthode qui reçoit un autre objet).
- Mon compte bancaire refuse les opérations invalides et garde un historique cohérent.