Sur cette page
- Dans cette leçon
- Chapitre 1 : Pourquoi nous avons besoin de la POO — code procédural face aux objets
- Chapitre 2 : Class et Object — définir un modèle et créer des objets à partir de lui
- Chapitre 3 :
__init__etself— un objet qui naît complet - Chapitre 4 : Les méthodes — les actions de l’objet
- Chapitre 5 : Les données partagées — Instance Attribute face à Class Attribute
- Chapitre 6 : Ce qui devient une classe — d’une exigence verbale à des classes
- Chapitre 7 : Retour à la banque — tout ensemble
- Exercices
- Mini-projet: Une petite bibliothèque
Classes et objets : du problème à une classe complète
- Reconnaître les problèmes du code procédural quand le système grandit, et expliquer comment la POO les résout
- Définir une classe et créer plusieurs objets à partir d’elle, et distinguer une classe d’un objet
- Écrire un
__init__qui initialise un objet complet et vérifie les valeurs, et expliquer ce qu’estself - Ajouter des méthodes — Getters et Setters — et un
__str__qui affiche l’objet - Distinguer un Instance Attribute d’un Class Attribute, et utiliser un compteur partagé
- Transformer une description verbale en structure de classes — ce qui est une classe, un attribut et une méthode
- Construire une classe complète qui relie toutes les idées — dans un mini-projet
Dans cette leçon
Cette leçon est construite comme une seule histoire, dans le même ordre que les diapositives. Nous partons d’un petit système bancaire écrit avec des dictionnaires et des fonctions, nous voyons où il casse, et à chaque chapitre nous ajoutons un outil qui résout un problème :
- Chapitre 1 — pourquoi nous avons besoin de la POO : les problèmes du code procédural
- Chapitre 2 — Class et Object : un modèle et des instances
- Chapitre 3 —
__init__etself: un objet qui naît complet - Chapitre 4 — les méthodes : l’objet ne fait pas que savoir, il agit aussi
- Chapitre 5 — les données partagées : les Class Attributes
- Chapitre 6 — ce qui devient une classe : d’une exigence verbale à des classes
- Chapitre 7 — retour à la banque : tout ensemble
À la fin de la leçon — de la pratique, et un mini-projet qui relie tous les chapitres.
Chapitre 1 : Pourquoi nous avons besoin de la POO — code procédural face aux objets
Qu’allons-nous apprendre ?
Toute cette partie du cours est consacrée à la POO — la programmation orientée objet (Object-Oriented Programming, OOP). Avant d’écrire une seule ligne de code POO, nous allons répondre à la question la plus importante : pourquoi ?
Dans ce chapitre :
- Nous allons écrire un petit système sans POO, de la façon que nous connaissons déjà.
- Nous verrons où il commence à poser problème.
- Nous comprendrons l’idée de la POO comme solution, encore sans syntaxe.
Pourquoi en avons-nous besoin ?
Tout ce que nous avons appris jusqu’ici fonctionne. On peut écrire n’importe quel programme avec des variables, des dicts, des listes et des fonctions. Cela s’appelle la programmation procédurale (Procedural Programming) : les données d’un côté, les fonctions qui agissent sur elles de l’autre.
Mais quand un système grandit, jusqu’à des dizaines de types de « choses » et des centaines de fonctions, les problèmes commencent. La POO est la façon dont presque tous les grands systèmes sont construits. Et les outils que vous utiliserez (FastAPI, les bibliothèques Python) sont eux aussi écrits en POO. Pour les lire et les utiliser, il faut la comprendre.
Explication à partir des bases
Le problème : un système de comptes bancaires — sans POO
Construisons un petit système de gestion de comptes bancaires, avec ce que nous connaissons déjà :
account1 = {"owner": "Dana", "balance": 1000}
account2 = {"owner": "Avi", "balance": 500}
def deposit(account, amount):
if amount <= 0:
raise ValueError("amount must be positive")
account["balance"] = account["balance"] + amount
def withdraw(account, amount):
if amount > account["balance"]:
raise ValueError("insufficient funds")
account["balance"] = account["balance"] - amount
deposit(account1, 200)
withdraw(account2, 100)
print(account1, account2){'owner': 'Dana', 'balance': 1200} {'owner': 'Avi', 'balance': 400}Ça fonctionne ! Alors où est le problème ? Agrandissons le système.
Problème 1 : tout le monde peut « toucher » aux données
account1["balance"] = -999999
account2["balnce"] = 100- La première ligne contourne toutes les vérifications que nous avons écrites dans
withdraw. Un solde négatif, sans aucune vérification. - La deuxième ligne contient une faute de frappe. Une nouvelle clé
"balnce"a été créée, sans aucune erreur (nous l’avons vu dans le thème structures de données et fonctions).
Les règles (ne pas retirer plus que ce qu’il y a, un montant positif) n’existent que si tout le monde pense à utiliser les fonctions. Rien ne les impose.
Problème 2 : les données et les actions sont dispersées
Chaque action sur un compte est une fonction séparée qui reçoit le dict : deposit(account, ...), withdraw(account, ...), transfer(...), monthly_fee(...), print_statement(...). Dans un vrai système, ce sont des dizaines de fonctions dans plusieurs fichiers. Celui qui reçoit un dict de compte ne sait pas quelles actions existent dessus ni où elles se trouvent.
Problème 3 : il n’y a pas de « structure » définie
À quoi ressemble un compte ? Qu’est-ce qui doit s’y trouver ? Peut-être qu’à un endroit du code quelqu’un a créé {"owner": ..., "balance": ...}, et à un autre endroit {"name": ..., "money": ...}. Il n’y a pas un seul endroit qui définit « voici un compte bancaire, voici ses champs, et voici ses actions ».
Problème 4 : des types semblables mais différents
Un « compte d’épargne » a été ajouté : comme un compte ordinaire, mais avec des intérêts, et sans possibilité de retirer en dessous de 1 000. Maintenant withdraw doit vérifier : if account["type"] == "savings": ... elif .... Et à chaque nouveau type de compte, toutes les fonctions se remplissent de if.
L’idée de la POO
La POO organise le code autour de « choses » (des objets), et non autour d’actions. Chaque « chose » contient ensemble :
- Ses données : les attributs (Attributes). Pour un compte : le titulaire, le solde.
- Les actions que l’on peut faire avec elle : les méthodes (Methods). Pour un compte : déposer, retirer.
PROCEDURAL OBJECT-ORIENTED
data: account dict ┌──────────────────────────┐
functions: deposit(account, x) │ BankAccount │
withdraw(account, x) │ data: owner, balance│
print_statement(account) │ actions: deposit() │
│ withdraw() │
"functions act ON data" │ statement() │
└──────────────────────────┘
"the object knows its own data
and its own actions"Et comment résout-elle les problèmes ?
| Problème | La solution POO | Nous l’apprendrons dans… |
|---|---|---|
| Tout le monde touche aux données | L’objet protège ses données, et les modifications passent par ses actions (l’encapsulation) | La leçon suivante |
| Les données et les actions sont dispersées | Tout est dans la classe. account. + Tab dans VS Code montre tout ce que l’on peut faire | Aujourd’hui |
| Pas de structure définie | Une seule classe définit exactement quelles données a chaque compte | Aujourd’hui |
| Des types semblables | L’héritage et le polymorphisme : un compte d’épargne « hérite » d’un compte ordinaire et ne change que ce qui est différent | Les leçons sur l’héritage et le polymorphisme |
Un modèle face à une chose précise
L’idée la plus fondamentale de la POO existe déjà dans le monde réel :
| Modèle (Class) | Choses précises créées à partir de lui (Objects) |
|---|---|
| Le plan de construction d’un appartement | L’appartement de Dana au 3e étage, celui d’Avi au 5e |
| Le formulaire « informations de l’étudiant » | Le formulaire rempli par Dana, celui rempli par Avi |
| Un modèle de fabrication de voitures sur une chaîne | La voiture rouge immatriculée 12-345-67 |
| Le concept « compte bancaire » | Le compte de Dana avec 1 200 NIS, celui d’Avi avec 400 NIS |
- Tous les appartements construits selon le même plan ont la même structure (pièces, cuisine), mais des valeurs différentes (couleur des murs, qui y habite).
- Une modification de l’appartement de Dana ne change pas celui d’Avi.
- Le plan lui-même n’est pas un appartement. On ne peut pas habiter dans un plan.
Class = un modèle de fabrication de voitures. Il définit que chaque voiture a une couleur, un modèle et une plaque d’immatriculation (les attributs), et que chaque voiture sait rouler, freiner et klaxonner (les méthodes). Object = une voiture précise fabriquée à partir du modèle : rouge, modèle 2024, plaque 12-345-67. On peut en fabriquer des milliers, chacune avec sa propre couleur et son propre numéro.
Syntaxe
Nous n’écrivons pas encore de classe. Dans le chapitre suivant, nous apprendrons la syntaxe complète. Mais voici un « avant-goût », pour voir où nous allons. Le même système bancaire, en POO :
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("amount must be positive")
self.balance = self.balance + amount
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("insufficient funds")
self.balance = self.balance - amount
account1 = BankAccount("Dana", 1000)
account1.deposit(200)
print(account1.balance)N’essayez pas de comprendre chaque mot maintenant (class, __init__, self), car chacun a un chapitre entier plus loin dans la leçon. Remarquez seulement une chose : les données (owner, balance) et les actions (deposit, withdraw) sont au même endroit, et on les appelle à travers l’objet : account1.deposit(200).
Premier exemple de code
Vous utilisez des objets depuis le début, sans le savoir ! En Python, chaque valeur est un objet, avec ses propres données et méthodes :
name = "dana cohen"
print(name.title())
print(name.upper())
print(type(name))
numbers = [3, 1, 2]
numbers.append(5)
numbers.sort()
print(numbers)
print(type(numbers))Sortie :
Dana Cohen
DANA COHEN
<class 'str'>
[1, 2, 3, 5]
<class 'list'>Analyse du code ligne par ligne
| Code | Ce que c’est en POO |
|---|---|
name = "dana cohen" | Un objet de type str a été créé. Ses données : les caractères. |
name.title() | Un appel à la méthode title de l’objet. Elle « sait » travailler sur son propre texte. |
type(name) → <class 'str'> | La classe (le modèle) de l’objet est str. |
numbers.append(5) | Une méthode d’un objet list qui modifie ses propres données. |
<class 'list'> | list est aussi une classe. Chaque liste que vous avez créée est un objet de la classe list. |
Point clé : str et list sont des classes que Python a écrites pour nous. Dans cette partie, nous apprendrons à écrire nos propres classes : BankAccount, Student, Product, adaptées à notre problème.
Un autre exemple
Un autre problème procédural : un système d’étudiants. Prenez le code suivant, et essayez de repérer les problèmes vous-même.
student1 = {"name": "Dana", "grades": [90, 85]}
student2 = {"name": "Avi", "grades": [70]}
student3 = {"nmae": "Noa", "grades": []}
def add_grade(student, grade):
student["grades"].append(grade)
def average(student):
return sum(student["grades"]) / len(student["grades"])
add_grade(student1, 100)
student2["grades"].append(500)
print(average(student1))
print(average(student2))
print(average(student3))91.66666666666667
285.0
Traceback (most recent call last):
...
ZeroDivisionError: division by zeroLes problèmes :
student3a été créé avec une faute de frappe ("nmae"). Aucun endroit ne définit ce qui « doit » se trouver dans un étudiant.student2["grades"].append(500)a contournéadd_grade. Même siadd_gradevérifiait que la note est entre 0 et 100, la vérification ne s’exécuterait pas. Une moyenne de 285 !averageplante sur un étudiant sans notes. Et celui qui utilise un dict d’étudiant doit savoir que cette fonction existe, et penser à gérer ce cas.
En POO, Student sera une seule classe avec une structure définie (un nom et des notes, toujours), avec une méthode add_grade qui est le seul moyen d’ajouter une note (et qui la vérifie), et avec une méthode average qui sait gérer une liste vide. Les règles se trouvent à côté des données qu’elles protègent.
Résumé
- Programmation procédurale : les données (dicts) et les fonctions séparément. Ça fonctionne, mais quand le système grandit, les données sont exposées, les actions dispersées, et il n’y a pas de structure contraignante.
- POO : organiser le code autour de choses. Chaque objet contient ensemble des données (attributs) et des actions (méthodes).
- Class = un modèle (un plan de construction, un modèle de fabrication). Object = une chose précise créée à partir de lui.
- En Python, chaque valeur est un objet :
stretlistsont des classes, et"hi".upper()est un appel de méthode. - La POO est une façon de penser : quelles choses existent, ce qu’elles savent, et ce qu’elles font. Tout n’a pas besoin d’être une classe.
Documentation officielle
Chapitre 2 : Class et Object — définir un modèle et créer des objets à partir de lui
Qu’allons-nous apprendre ?
- Ce qu’est une classe (Class) et ce qu’est un objet (Object, aussi appelé Instance).
- Ce que signifie le mot
class. - La syntaxe pour définir une classe.
- Comment créer un objet à partir d’une classe.
- Pourquoi chaque objet est indépendant.
Pourquoi en avons-nous besoin ?
Dans le chapitre précédent, nous avons vu que le problème avec « dict + fonctions » est qu’il n’y a pas de structure définie ni d’endroit unique qui rassemble les données et les actions. La classe est l’outil qui crée cette structure. Tout le reste (__init__, self, les méthodes) est construit sur elle.
Explication à partir des bases
Qu’est-ce qu’une classe ?
Une classe est un modèle (Blueprint) qui définit un nouveau type de chose : quelles informations aura chaque chose de ce type, et ce qu’elle saura faire.
- Une classe ne contient pas les données d’une chose précise. C’est seulement la définition.
- À partir d’une seule classe, on peut créer autant de choses qu’on veut.
- Quand on définit une classe, on crée en fait un nouveau type en Python, exactement comme
stroulist.
Qu’est-ce qu’un objet (Instance) ?
Un objet est une chose précise créée à partir d’une classe. Il a des données qui lui sont propres.
Instance est un synonyme : « un objet de la classe Student » = « une instance de Student ». L’acte de créer un objet à partir d’une classe s’appelle l’instanciation (Instantiation).
| Class | Object / Instance | |
|---|---|---|
| Ce que c’est | Un modèle, une définition | Une chose précise |
| Combien | Une seule pour chaque type | Autant qu’on veut |
| Données | Définit quels champs il y aura | Contient des valeurs précises |
| Exemple | Student | Dana, Avi, Noa |
| Analogie | Un modèle de fabrication de voitures | La voiture rouge immatriculée 12-345-67 |
Le mot class
class est un mot réservé (Keyword) de Python, comme def, if et for. Il dit à Python : « ici commence la définition d’un nouveau type ». Ensuite vient le nom de la classe, puis un corps indenté.
Les noms de classes
Selon PEP 8, les noms de classes s’écrivent en PascalCase : chaque mot commence par une majuscule, sans tirets bas. Student, BankAccount, ShoppingCart. C’est différent des fonctions et des variables (snake_case), et ainsi on voit tout de suite si quelque chose est une classe.
Syntaxe
class ClassName:
# class body - indented
pass
my_object = ClassName()| Partie | Explication |
|---|---|
class | Un mot réservé : « une définition de classe commence ». |
ClassName | Le nom de la classe, en PascalCase. |
: | Obligatoire, comme après def ou if. |
| Un corps indenté | Ce que contient le modèle (dans les chapitres suivants : __init__ et des méthodes). |
pass | « Ne rien faire ». Le corps d’une classe (ou d’une fonction) ne peut pas être vide, donc pass occupe la place. |
ClassName() | Créer un objet : on appelle la classe avec des parenthèses, comme une fonction. On obtient un nouvel objet. |
Premier exemple de code
week-3/first_class.py :
class Person:
pass
dana = Person()
avi = Person()
print(dana)
print(avi)
print(type(dana))
print(dana is avi)
print(isinstance(dana, Person))Sortie :
<__main__.Person object at 0x000001F4A3B2C1D0>
<__main__.Person object at 0x000001F4A3B2C2E0>
<class '__main__.Person'>
False
TrueAnalyse du code ligne par ligne
| Ligne | Explication |
|---|---|
class Person: | Définit un nouveau type nommé Person. Aucune personne n’a encore été créée. |
pass | Le modèle est vide pour l’instant. |
dana = Person() | Instanciation : un nouvel objet de type Person a été créé, et le nom dana pointe vers lui. |
avi = Person() | Un autre objet, entièrement séparé. |
print(dana) | L’objet n’a pas de « contenu » à afficher, donc Python affiche une description par défaut : le type, et une adresse en mémoire (0x...). Votre adresse sera différente. |
| Deux adresses différentes | Deux objets séparés en RAM. |
type(dana) | <class '__main__.Person'>. La classe de l’objet est Person, définie dans le fichier principal (__main__). |
dana is avi | False : ce n’est pas le même objet (souvenez-vous de is !). |
isinstance(dana, Person) | True : « dana est-il une instance de Person ? » |
Un autre exemple
Ajouter des données à un objet — et voir pourquoi ce n’est pas la bonne façon. On peut attacher un attribut à un objet de l’extérieur, avec un point :
class Product:
pass
laptop = Product()
laptop.name = "Laptop"
laptop.price = 3500
mouse = Product()
mouse.name = "Mouse"
mouse.price = 80
print(laptop.name, laptop.price)
print(mouse.name, mouse.price)
mouse.price = 70
print("After update:", mouse.price, "| laptop unchanged:", laptop.price)Sortie :
Laptop 3500
Mouse 80
After update: 70 | laptop unchanged: 3500Analyse :
laptop.name = "Laptop"— crée un attribut nomménamesur l’objetlaptop. Le point signifie « de » :laptop.name= « le nom du portable ».- Chaque objet a des attributs qui lui sont propres. Modifier
mouse.pricene touche paslaptop. - C’est déjà mieux qu’un dict :
laptop.priceest plus lisible quelaptop["price"].
Mais il y a ici un gros problème. Qu’est-ce qui garantit que chaque Product a un name et un price ?
keyboard = Product()
keyboard.nmae = "Keyboard"
print(keyboard.name)AttributeError: 'Product' object has no attribute 'name'- Une faute de frappe (
nmae) a créé un attribut différent. - Nous avons oublié de définir
price, et il n’y a aucun avertissement tant que personne n’essaie de l’utiliser. - C’est exactement le problème du dict du chapitre précédent ! La classe vide ne définit toujours pas de structure.
La solution : la classe elle-même doit définir quelles données chaque objet doit avoir, et les remplir au moment de la création. C’est le rôle de __init__, le sujet du chapitre suivant.
Résumé
- Class = un modèle qui définit un nouveau type. Object / Instance = une chose précise créée à partir de lui.
- Syntaxe :
class Name:+ un corps indenté (oupass). Les noms en PascalCase. - Créer un objet :
obj = Name(), avec des parenthèses. Sans parenthèses, c’est la classe elle-même. - Chaque objet est indépendant :
a is b→False. Les attributs de l’un n’affectent pas l’autre. type(obj)renvoie la classe.isinstance(obj, Name)vérifie l’appartenance.- Ajouter des attributs « de l’extérieur » fonctionne, mais ne définit pas de structure. C’est pourquoi nous avons besoin de
__init__.
Documentation officielle
Chapitre 3 : __init__ et self — un objet qui naît complet
Qu’allons-nous apprendre ?
- Ce qu’est
__init__, la fonction qui s’exécute automatiquement quand un objet est créé. - Ce qu’est un constructeur (Constructor).
- Ce qu’est
self, le mot qui apparaît partout en POO. - Les Instance Attributes : les données de chaque objet.
Pourquoi en avons-nous besoin ?
Dans le chapitre précédent, nous avons ajouté des attributs « de l’extérieur » : laptop.name = "Laptop". Nous avons vu que c’est dangereux : on peut oublier un champ, faire une faute de frappe, et il n’y a pas de structure contraignante. Nous avons même écrit une fonction d’aide make_book pour garantir que chaque livre a un title et des pages.
__init__ est exactement ce make_book, mais à l’intérieur de la classe. Il garantit que chaque objet est créé initialisé : avec tous ses champs, et avec des valeurs, dès le tout premier instant.
Explication à partir des bases
__init__ — la méthode d’initialisation
__init__ (prononcé « dunder init », de double underscore init) est une fonction spéciale définie à l’intérieur d’une classe. Elle :
- Est appelée automatiquement dès qu’un nouvel objet est créé :
Student(...). - Initialise l’objet, c’est-à-dire y met les données initiales.
- Ne renvoie rien (pas besoin de
return).
Le nom avec deux tirets bas de chaque côté marque une méthode spéciale de Python (appelée Dunder Method ou Magic Method). Python cherche exactement ce nom, et l’appelle tout seul. Vous n’écrivez jamais obj.__init__() vous-même.
Le constructeur
Constructeur (Constructor) = le code qui s’exécute quand un nouvel objet est construit. En Python, c’est __init__ qui joue ce rôle, on l’appelle donc généralement « le constructeur ». (Pour être techniquement précis : la construction elle-même, l’allocation de l’objet en mémoire, se fait une étape plus tôt, et __init__ « ne fait que » le remplir. Pour les besoins du cours, __init__ = constructeur.)
self — « cet objet »
self est le premier paramètre de chaque méthode d’une classe, y compris __init__. Il pointe vers l’objet précis sur lequel la méthode travaille en ce moment.
Pourquoi en a-t-on besoin ? La classe est un seul modèle, mais beaucoup d’objets sont créés à partir d’elle. Quand le code de __init__ dit « enregistre le nom », il doit savoir dans quel objet l’enregistrer. self est la réponse : « dans l’objet en cours de création maintenant ».
self.name = nameLisez-le ainsi : « dans cet objet (self), crée un attribut nommé name, et mets-y la valeur reçue dans le paramètre name ».
self.name— l’attribut de l’objet. Il reste tant que l’objet existe.name(seul) — le paramètre de__init__: une variable locale (Local, souvenez-vous de la portée !) qui disparaît à la fin de__init__.
D’où vient self ? Python le passe automatiquement ! Quand vous écrivez Student("Dana", 24), vous passez 2 arguments, mais __init__ en reçoit 3 : Python ajoute le nouvel objet en tant que self, au début.
La classe est un formulaire vierge avec des champs : « Nom : ____ Âge : ____ ». __init__ est l’employé des inscriptions qui remplit un nouveau formulaire pour chaque étudiant qui arrive. self est « le formulaire sur le bureau en ce moment » : l’employé écrit « Nom : Dana » sur ce formulaire-là, et non sur tous les formulaires de l’armoire. Demain Avi arrivera, et un autre formulaire sera rempli, mais l’employé l’appellera de nouveau « le formulaire sur le bureau » (self).
Ce qui se passe à la ligne dana = Student("Dana", 24)
- Python crée un nouvel objet vide de type
Studenten mémoire. - Python appelle
__init__et lui passe :self= le nouvel objet,name = "Dana",age = 24. __init__exécuteself.name = name, c’est-à-dire que lenamedu nouvel objet ="Dana". Et de même pourage.__init__se termine. Les paramètres locaux disparaissent, et les attributs restent sur l’objet.- Python renvoie l’objet, et le nom
danapointe vers lui.
Les Instance Attributes
Instance Attributes = des données qui appartiennent à un objet précis. Chaque objet a ses propres valeurs. On les définit dans __init__ avec self.something = ....
Syntaxe
class ClassName:
def __init__(self, param1, param2):
self.attribute1 = param1
self.attribute2 = param2
self.attribute3 = "default value" # attributes do not have to come from parameters
obj = ClassName(arg1, arg2) # Python calls __init__(obj, arg1, arg2) for you
obj.attribute1 # read an instance attribute__init__s’écrit à l’intérieur de la classe, indenté.selfest toujours en premier dans la liste des paramètres.- Lors de l’appel, on ne passe pas
self. - On peut donner des valeurs par défaut aux paramètres, comme dans n’importe quelle fonction :
def __init__(self, name, age=18):.
Premier exemple de code
week-3/student.py :
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
self.grades = []
dana = Student("Dana", 24)
avi = Student("Avi", 31)
print(dana.name, dana.age, dana.grades)
print(avi.name, avi.age, avi.grades)
dana.grades.append(95)
print("Dana's grades:", dana.grades)
print("Avi's grades:", avi.grades)Sortie :
Dana 24 []
Avi 31 []
Dana's grades: [95]
Avi's grades: []Analyse du code ligne par ligne
| Ligne | Explication |
|---|---|
class Student: | La définition du modèle. |
def __init__(self, name, age): | Le constructeur. self = l’objet en cours de création. name, age = ce que l’on passe à la création. |
self.name = name | Un attribut name sur l’objet, avec la valeur du paramètre. |
self.age = age | De même pour l’âge. |
self.grades = [] | Un attribut qui ne vient pas d’un paramètre : chaque étudiant commence avec sa propre liste de notes vide. |
dana = Student("Dana", 24) | Python : crée un objet, appelle __init__(new_obj, "Dana", 24), et le renvoie. |
avi = Student("Avi", 31) | Un deuxième objet. __init__ s’exécute à nouveau, avec self = le deuxième objet. |
dana.grades.append(95) | Accès à la liste de Dana et ajout dans celle-ci. |
avi.grades reste [] | Chaque __init__ a créé une nouvelle liste, il n’y a donc pas de partage. (Comparez avec le bug de def f(cart=[]) dans la leçon sur les fonctions !) |
Maintenant la structure est garantie : on ne peut pas créer un Student sans nom ni âge, et chaque étudiant a toujours des grades.
Un autre exemple
Un produit avec des valeurs par défaut, un attribut calculé et une première validation. week-3/product.py :
class Product:
def __init__(self, name, price, quantity=0):
if price < 0:
raise ValueError("price cannot be negative, got " + str(price))
self.name = name
self.price = price
self.quantity = quantity
self.in_stock = quantity > 0
laptop = Product("Laptop", 3500, 4)
cable = Product("USB Cable", 25)
print(laptop.name, laptop.price, laptop.quantity, laptop.in_stock)
print(cable.name, cable.price, cable.quantity, cable.in_stock)
try:
broken = Product("Ghost", -10)
except ValueError as error:
print("Could not create product:", error)Sortie :
Laptop 3500 4 True
USB Cable 25 0 False
Could not create product: price cannot be negative, got -10Analyse :
quantity=0— une valeur par défaut.Product("USB Cable", 25)reçoit une quantité de 0.self.in_stock = quantity > 0— un attribut calculé à partir des données :TrueouFalse.- La validation dans le constructeur :
__init__refuse de créer un produit avec un prix négatif, et lance uneValueError(souvenez-vous deraise!). Un produit invalide ne peut pas exister dans le système. C’est exactement « garder les règles à côté des données » dont nous avons parlé au premier chapitre. - Quand
__init__lance une erreur, l’objet n’est pas créé, etbrokenne reçoit pas de valeur.
Person — un troisième exemple, pour consolider
class Person:
def __init__(self, first_name, last_name, city="Unknown"):
self.first_name = first_name
self.last_name = last_name
self.city = city
self.full_name = first_name + " " + last_name
noa = Person("Noa", "Levi", "Haifa")
tom = Person("Tom", "Cohen")
print(noa.full_name, "-", noa.city)
print(tom.full_name, "-", tom.city)Noa Levi - Haifa
Tom Cohen - UnknownRésumé
__init__= le constructeur : appelé automatiquement à la création d’un objet, il initialise ses données.self= l’objet précis sur lequel la méthode travaille en ce moment. Toujours le premier paramètre, et Python le passe tout seul.self.x = x:self.x= un attribut enregistré sur l’objet.x= un paramètre local qui disparaît.obj = Class(a, b)→ Python crée un objet, appelle__init__(obj, a, b), et renvoie l’objet.- Dans
__init__, on peut avoir : des valeurs par défaut, des attributs calculés, et une validation avecraise, pour qu’un objet invalide ne soit jamais créé.
Documentation officielle
Chapitre 4 : Les méthodes — les actions de l’objet
Qu’allons-nous apprendre ?
- Ce qu’est une méthode (Method).
- Comment la définir et comment l’appeler.
- Trois types de méthodes selon ce qu’elles font : elles calculent, elles modifient l’état, elles affichent.
- Une méthode qui appelle une autre méthode.
__str__: comment un objet « se présente ».
Pourquoi en avons-nous besoin ?
Jusqu’ici, nos objets n’avaient que des données. Mais l’idée de la POO, c’est données + actions ensemble. Au premier chapitre, nous avons vu que les règles (comme « on ne peut pas retirer plus que le solde ») doivent se trouver à côté des données. Les méthodes sont l’endroit où vivent ces règles.
Et il reste un problème ouvert du chapitre précédent : la description de la voiture était calculée une seule fois dans __init__, et devenait obsolète quand la couleur changeait. Une méthode recalcule à chaque fois.
Explication à partir des bases
Qu’est-ce qu’une méthode ?
Méthode = une fonction définie à l’intérieur d’une classe, qui agit sur un objet précis de cette classe.
| Function (une fonction ordinaire) | Method | |
|---|---|---|
| Où elle est définie | Au niveau supérieur du fichier | Dans une classe |
| Comment on l’appelle | function_name(args) | obj.method_name(args) |
| Sur quoi elle travaille | Sur ce qu’on lui passe | Sur son objet (self), et sur ce qu’on lui passe |
| Le premier paramètre | Ce que vous voulez | self |
Vous avez déjà utilisé des méthodes : "hi".upper(), numbers.append(5). Aujourd’hui, nous écrivons les nôtres.
self dans les méthodes
Exactement comme dans __init__ : quand vous appelez dana.add_grade(90), Python passe automatiquement dana en tant que self. Dans la méthode, self.grades = les notes de Dana. Si vous appelez avi.add_grade(80), exactement la même méthode s’exécute, mais self est Avi.
dana.add_grade(90) → Student.add_grade(dana, 90)
↑ ↑
self gradeLa ligne de gauche est ce que nous écrivons. La ligne de droite est ce que Python fait réellement.
Trois types de méthodes (selon ce qu’elles font)
| Type | Ce qu’elle fait | Exemple |
|---|---|---|
| Getter | Calcule et renvoie une valeur à partir des données, sans rien modifier | average(), is_passing() |
| Setter | Modifie l’état de l’objet | add_grade(90), deposit(100) |
| Affichage | Affiche / renvoie du texte à afficher | print_report(), __str__() |
Une distinction utile du Clean Code : une bonne méthode est généralement soit un Getter soit un Setter, pas les deux à la fois (SRP !).
L’état (State)
L’état = les valeurs de tous les attributs de l’objet à un instant donné. Un compte avec un solde de 1 000 est dans un certain état. Après deposit(200), il est dans un nouvel état. Les méthodes Setter sont la façon ordonnée de modifier l’état.
__str__ — à quoi ressemble l’objet à l’affichage
Vous vous souvenez que print(dana) affichait <__main__.Student object at 0x...> ? Si vous définissez dans la classe une méthode spéciale nommée __str__ (encore un Dunder !) qui renvoie une chaîne, print et str() l’utiliseront pour afficher l’objet.
Syntaxe
class ClassName:
def __init__(self, ...):
self.data = ...
def getter_method(self): # returns something, changes nothing
return ... self.data ...
def setter_method(self, value): # changes the object's state
self.data = ...
def uses_other_method(self):
return self.getter_method() * 2 # call another method on the same object
def __str__(self): # used by print() and str()
return "readable text about " + str(self.data)
obj = ClassName(...)
obj.setter_method(5) # no self in the call
result = obj.getter_method()
print(obj) # calls __str__ automatically- Chaque méthode est indentée dans la classe, et
selfest en premier. - Entre les méthodes, mettez une ligne vide (PEP 8).
- Dans une méthode, on accède aux données et aux autres méthodes avec
self..
Premier exemple de code
week-3/student_methods.py :
PASSING_GRADE = 60
class Student:
def __init__(self, name):
self.name = name
self.grades = []
def add_grade(self, grade):
if grade < 0 or grade > 100:
raise ValueError("grade must be between 0 and 100, got " + str(grade))
self.grades.append(grade)
def average(self):
if not self.grades:
return 0
return sum(self.grades) / len(self.grades)
def is_passing(self):
return self.average() >= PASSING_GRADE
def __str__(self):
return self.name + " (average " + str(round(self.average(), 1)) + ")"
dana = Student("Dana")
dana.add_grade(90)
dana.add_grade(75)
avi = Student("Avi")
avi.add_grade(50)
print(dana)
print(avi)
print("Dana passing?", dana.is_passing())
print("Avi passing?", avi.is_passing())Sortie :
Dana (average 82.5)
Avi (average 50.0)
Dana passing? True
Avi passing? FalseAnalyse du code ligne par ligne
| Code | Type | Explication |
|---|---|---|
def add_grade(self, grade): | Setter | Modifie l’état : ajoute à self.grades. Et vérifie d’abord la règle (0–100). |
def average(self): | Getter | Calcule à partir des données de l’objet et renvoie. Ne modifie rien. Gère une liste vide (le bug du premier chapitre !). |
def is_passing(self): | Getter | Appelle une autre méthode du même objet : self.average(). |
def __str__(self): | Affichage | Renvoie une chaîne. print(dana) l’appelle automatiquement. |
dana.add_grade(90) | Python : Student.add_grade(dana, 90). self = dana. | |
print(avi) | Python : print(avi.__str__()), c’est-à-dire "Avi (average 50.0)". |
Le point important : le code qui utilise la classe (dana.add_grade(...)) n’a pas besoin de savoir comment les notes sont stockées (une liste), ni de vérifier la plage lui-même. C’est la classe qui en est responsable.
Un autre exemple
Un compte bancaire — le problème du premier chapitre, résolu. week-3/bank_account.py :
class BankAccount:
def __init__(self, owner, balance=0):
if balance < 0:
raise ValueError("initial balance cannot be negative")
self.owner = owner
self.balance = balance
self.transactions = []
def deposit(self, amount):
if amount <= 0:
raise ValueError("deposit must be positive, got " + str(amount))
self.balance = self.balance + amount
self.transactions.append("deposit " + str(amount))
def withdraw(self, amount):
if amount <= 0:
raise ValueError("withdrawal must be positive, got " + str(amount))
if amount > self.balance:
raise ValueError("insufficient funds: balance " + str(self.balance) + ", requested " + str(amount))
self.balance = self.balance - amount
self.transactions.append("withdraw " + str(amount))
def transfer_to(self, other_account, amount):
self.withdraw(amount)
other_account.deposit(amount)
def statement(self):
print("--- Statement for", self.owner, "---")
for transaction in self.transactions:
print(" ", transaction)
print(" Balance:", self.balance)
def __str__(self):
return "Account of " + self.owner + ": " + str(self.balance) + " NIS"
dana = BankAccount("Dana", 1000)
avi = BankAccount("Avi")
dana.deposit(200)
dana.transfer_to(avi, 300)
try:
avi.withdraw(5000)
except ValueError as error:
print("Rejected:", error)
print(dana)
print(avi)
dana.statement()Sortie :
Rejected: insufficient funds: balance 300, requested 5000
Account of Dana: 900 NIS
Account of Avi: 300 NIS
--- Statement for Dana ---
deposit 200
withdraw 300
Balance: 900Analyse :
transfer_to(self, other_account, amount)— une méthode qui reçoit un autre objet en paramètre. Elle ne modifie pas directement les soldes. Elle utiliseself.withdrawetother_account.deposit, et ainsi toutes les vérifications s’exécutent automatiquement. DRY !- Si
withdrawéchoue (pas assez d’argent), elle lance une erreur avant ledeposit, donc aucun « argent n’est créé à partir de rien ». self.transactions— un historique interne que chaque action met à jour. Celui qui utilise le compte n’a pas besoin de penser à enregistrer une transaction.statement()est une méthode d’affichage.__str__donne une courte description pourprint.- Comparez avec la version procédurale du premier chapitre : les mêmes actions, mais il y a maintenant un seul endroit qui définit ce qu’est un compte et ce qu’on a le droit d’en faire.
Résumé
- Méthode = une fonction dans une classe. On la définit avec
selfen premier, et on appelleobj.method(args)sansself. obj.method(x)équivaut àClass.method(obj, x). C’est ainsi queself« sait » sur quel objet travailler.- Dans une méthode,
self.attributeetself.other_method(). - Un Getter renvoie et ne modifie rien. Un Setter modifie l’état (et vérifie les règles).
__str__renvoie une chaîne, etprint(obj)l’utilise.- Une information dérivée se calcule mieux dans une méthode, plutôt que d’être stockée comme un attribut qui risque de devenir obsolète.
Documentation officielle
Chapitre 5 : Les données partagées — Instance Attribute face à Class Attribute
Qu’allons-nous apprendre ?
- Attribut face à méthode : comment décider de ce qu’est chaque chose.
- Instance Attribute : les données de chaque objet séparément (ce que nous avons fait jusqu’ici).
- Class Attribute : les données de la classe, partagées par tous les objets.
- Quand utiliser chacun, et quels pièges existent.
Pourquoi en avons-nous besoin ?
Supposons qu’il y ait une classe Product, et que tous les produits soient soumis à la même TVA : 17 %. Où garder la TVA ?
- Comme Instance Attribute (
self.vat = 0.17) ? Alors il y a mille copies du même nombre, et si la TVA change, il faut mettre à jour mille objets. - Comme constante globale en haut du fichier ? Possible, mais elle est « loin » de la classe à laquelle elle appartient.
Et si l’on veut savoir combien de produits ont été créés au total ? Aucun produit ne le « sait » à lui seul. C’est une information sur tout le type. Les Class Attributes résolvent les deux cas.
Explication à partir des bases
Attribut ou méthode ?
| Attribute | Method | |
|---|---|---|
| Ce que c’est | Une donnée, ce que l’objet sait / contient | Une action, ce que l’objet fait / calcule |
| Nature grammaticale | Un nom / un adjectif | Un verbe |
| Exemples | name, price, balance, is_active | deposit(), add_grade(), total() |
| Accès | obj.price | obj.total() avec des parenthèses |
Des questions qui aident à décider :
- Est-ce une donnée stockée, ou quelque chose de calculé ? Une donnée stockée → un attribut. Calculé à partir d’autres données → généralement une méthode (nous l’avons vu : la
descriptionqui devenait obsolète). - Cela modifie-t-il l’objet ? Toute modification ordonnée → une méthode (une commande).
- Faut-il des règles lors de la modification ? Si oui, la modification passe par une méthode (et nous l’approfondirons dans la leçon sur l’encapsulation).
Instance Attribute
Défini avec self.x = ..., généralement dans __init__. Chaque objet a sa propre copie, avec sa propre valeur.
Class Attribute
Défini directement dans le corps de la classe, en dehors de toute méthode, sans self :
class Product:
VAT_RATE = 0.17 # class attribute - one shared value
count = 0 # class attribute
def __init__(self, name):
self.name = name # instance attribute - per object- Il n’y a qu’une seule copie, qui appartient à la classe elle-même.
- Tous les objets la voient.
- On y accède par la classe (
Product.VAT_RATE, la façon recommandée), ou par un objet (laptop.VAT_RATE, fonctionne en lecture).
Les Instance Attributes = ce que chaque élève garde pour lui : un nom, des notes, un cahier. Les Class Attributes = ce qui est affiché au tableau de la classe et partagé par tous : le nom du professeur principal, le numéro de la salle, et le nombre d’élèves dans la classe. Quand le professeur change, on met à jour le tableau une seule fois, et pas dans le cahier de chaque élève.
Comment Python trouve un attribut
Quand vous écrivez obj.x, Python cherche :
- D’abord sur l’objet (les Instance Attributes).
- S’il ne le trouve pas, sur la classe (les Class Attributes et les méthodes).
- Pas trouvé →
AttributeError.
(Remarquez : c’est semblable à LEGB de la leçon sur la portée, une recherche par couches.) C’est aussi de là que vient le piège de l’erreur courante.
Syntaxe
class ClassName:
SHARED_CONSTANT = 100 # class attribute (constant style: UPPER_CASE)
counter = 0 # class attribute (changes, shared)
def __init__(self, value):
self.value = value # instance attribute
ClassName.counter = ClassName.counter + 1 # update the class attribute through the CLASS
def uses_constant(self):
return self.value * ClassName.SHARED_CONSTANT
ClassName.SHARED_CONSTANT # read through the class
ClassName.counterPremier exemple de code
week-3/products_shared.py :
class Product:
VAT_RATE = 0.17
created_count = 0
def __init__(self, name, price):
self.name = name
self.price = price
Product.created_count = Product.created_count + 1
def price_with_vat(self):
return round(self.price * (1 + Product.VAT_RATE), 2)
laptop = Product("Laptop", 1000)
mouse = Product("Mouse", 50)
print(laptop.name, laptop.price_with_vat())
print(mouse.name, mouse.price_with_vat())
print("Products created:", Product.created_count)
Product.VAT_RATE = 0.18
print("After VAT change:", laptop.price_with_vat(), mouse.price_with_vat())Sortie :
Laptop 1170.0
Mouse 58.5
Products created: 2
After VAT change: 1180.0 59.0Analyse du code ligne par ligne
| Code | Type | Explication |
|---|---|---|
VAT_RATE = 0.17 | Class Attribute | Une seule valeur pour tous les produits. Les majuscules = « une constante » (une convention). |
created_count = 0 | Class Attribute | Un compteur partagé. Il commence à 0 quand la classe est définie. |
self.name, self.price | Instance Attributes | Différents pour chaque produit. |
Product.created_count = Product.created_count + 1 | Chaque __init__ met à jour le compteur de la classe, il compte donc tous les objets. | |
Product.VAT_RATE dans la méthode | La lecture d’un Class Attribute. | |
Product.VAT_RATE = 0.18 | Une seule modification, et tous les produits sont affectés immédiatement, car ils lisent tous au même endroit. |
Un autre exemple
Une classe avec un nombre de places limité, et un identifiant unique automatique. week-3/students_ids.py :
class Student:
SCHOOL_NAME = "Kodkod 6"
MAX_STUDENTS = 3
next_id = 1
all_students = []
def __init__(self, name):
if len(Student.all_students) >= Student.MAX_STUDENTS:
raise ValueError("class is full - cannot add " + name)
self.name = name
self.student_id = Student.next_id
Student.next_id = Student.next_id + 1
Student.all_students.append(self)
def __str__(self):
return "#" + str(self.student_id) + " " + self.name + " (" + Student.SCHOOL_NAME + ")"
for name in ["Dana", "Avi", "Noa", "Tom"]:
try:
Student(name)
except ValueError as error:
print("Error:", error)
for student in Student.all_students:
print(student)
print("Next id will be:", Student.next_id)Sortie :
Error: class is full - cannot add Tom
#1 Dana (Kodkod 6)
#2 Avi (Kodkod 6)
#3 Noa (Kodkod 6)
Next id will be: 4Analyse :
next_id— un compteur qui produit un identifiant unique pour chaque étudiant. Chaque objet garde son propre numéro dans l’Instance Attributestudent_id, et le compteur partagé avance.all_students— une liste partagée dans laquelle la classe « enregistre » chaque objet créé.append(self)= « ajoute cet objet à la liste ».MAX_STUDENTS— une règle qui s’applique à toute la classe. La vérification s’exécute avant l’attribution de l’identifiant, Tom n’a donc pas « brûlé » de numéro.Student(name)dans la boucle sans le stocker dans une variable : l’objet n’est pas perdu, car il est enregistré dansall_students.
Les Class Attributes sont excellents pour les constantes (VAT_RATE, MAX_STUDENTS) et pour les compteurs. Une liste partagée de « tous les objets » est un bon outil de démonstration, mais dans les vrais projets il vaut généralement mieux gérer la collection dans une classe séparée (par exemple School qui contient une liste d’étudiants). Nous l’apprendrons dans les leçons sur la conception orientée objet (la composition).
Résumé
- Attribute = ce que l’objet sait (un nom). Method = ce qu’il fait (un verbe, avec des parenthèses). Une information calculée → une méthode.
- Instance Attribute (
self.xdans__init__) = chaque objet a sa propre valeur. - Class Attribute (dans le corps de la classe, sans
self) = une seule copie partagée. Convient aux constantes et aux compteurs. - Python cherche
obj.xd’abord sur l’objet, puis sur la classe. - Modifiez un Class Attribute uniquement par le nom de la classe.
obj.X = ...crée un nouvel Instance Attribute. - Une liste/un dict « par objet » → dans
__init__, pas dans le corps de la classe.
Documentation officielle
Chapitre 6 : Ce qui devient une classe — d’une exigence verbale à des classes
Qu’allons-nous apprendre ?
Jusqu’ici, nous vous avons dit quelles classes écrire. Maintenant, nous passons de l’autre côté : c’est vous qui décidez.
- Comment trouver des classes dans une description verbale d’un système.
- Quatre questions qui décident : une classe, ou non ?
- Quand quelque chose est un attribut, et quand c’est une classe à part entière.
- À quelle classe appartient chaque action.
Pourquoi en avons-nous besoin ?
Le programme du cours insiste : le but est de comprendre la POO, et pas seulement de savoir recopier une classe. Au travail, personne ne vous dira « écris une classe nommée Order avec ces champs ». Vous recevrez une exigence : « il nous faut un système dans lequel des clients commandent des produits… », et vous devrez la transformer vous-mêmes en structure de classes.
Une bonne conception au départ fait gagner des semaines de corrections. Une mauvaise conception mène à une seule classe énorme, ou à des dizaines de classes vides qui ne font rien.
Explication à partir des bases
Outil 1 : l’analyse des noms et des verbes
Une technique simple et efficace : lisez l’exigence, et marquez :
| Dans le texte | Un candidat pour |
|---|---|
| Les noms importants (« client », « commande », « produit ») | Des classes ou des attributs |
| Les adjectifs et propriétés (« prix », « date », « actif ») | Des attributs |
| Les verbes (« commande », « paie », « annule ») | Des méthodes |
| Les règles (« il n’est pas possible de… », « au maximum… ») | La validation / les invariants |
C’est un point de départ, pas une réponse. Tous les noms ne deviennent pas des classes.
Outil 2 : quatre questions — est-ce une classe ?
| # | Question | Si la réponse est « oui » |
|---|---|---|
| 1 | A-t-il une identité et des données propres ? (chacun a des valeurs différentes) | Penche vers une classe |
| 2 | A-t-il un comportement ou des règles ? (actions, validation) | Penche fortement vers une classe |
| 3 | Y en a-t-il plusieurs instances ? (beaucoup de clients, beaucoup de commandes) | Penche vers une classe |
| 4 | Est-ce seulement une valeur unique, sans aucun comportement ? (« couleur », « nom ») | Un attribut, pas une classe |
« Voiture » — elle a sa propre plaque d’immatriculation (identité), elle roule et freine (comportement), et il y a beaucoup de voitures. ✓ Une classe. « La couleur de la voiture » — ce n’est qu’une valeur (« rouge »), sans actions propres. Un attribut de la voiture. « Moteur » — ça dépend ! Dans un jeu simple, un attribut (engine_size = 1.6). Dans un garage qui répare des moteurs, une classe (il a un numéro de série, un historique d’entretien et un état). Le même concept, une décision différente selon ce dont le système a besoin.
Outil 3 : un simple attribut ou une classe à part entière ?
Une question décisive : faut-il conserver plus d’une information sur cette chose, ou lui appliquer des règles ?
| Concept | Un simple attribut quand… | Une classe à part entière quand… |
|---|---|---|
| Adresse | Un seul texte suffit : "Herzl 10, Haifa" | Il faut la ville, la rue et le code postal séparément, filtrer par ville, et valider le code postal |
| Prix | Un seul nombre | Il faut une devise, des conversions et un arrondi selon la devise |
| Note | Un nombre | Il faut aussi une date, le nom de l’examen, un coefficient et un recours |
Règle empirique : commencez simple (un attribut). Quand la valeur « grandit » et acquiert plus de champs et de règles, transformez-la en classe.
Outil 4 : à quelle classe appartient la méthode ?
La règle d’or : la méthode va dans la classe qui détient les données dont elle a besoin. (On l’appelle « Information Expert ».)
- « Calculer le total de la commande » → a besoin des articles de la commande →
Order.total(). - « Le produit est-il en stock ? » → a besoin de la quantité du produit →
Product.is_in_stock(). - « Combien le client a-t-il dépensé cette année ? » → a besoin des commandes du client →
Customer(s’il détient ses propres commandes), ou une classe qui gère toutes les commandes.
Un signe d’alerte : une méthode de la classe A qui est surtout other.x, other.y, other.z → elle appartient probablement à la classe de other.
Syntaxe
Il n’y a pas de nouvelle syntaxe. Le produit de cette étape est une conception, que l’on peut écrire sous forme de « squelette » de code, avec pass et des docstrings :
class Order:
"""A customer's order: items, status and total."""
def __init__(self, customer):
self.customer = customer # has-a: the order holds its customer
self.items = []
self.status = "new"
def add_item(self, product, quantity):
pass
def total(self):
passUn squelette comme celui-ci permet de réfléchir à la structure avant d’écrire la logique.
Premier exemple de code
L’exigence : « Un système pour un café. Des clients commandent des boissons dans le menu. Chaque boisson a un nom, une taille (petite/grande) et un prix. Une grande boisson coûte 3 NIS de plus. Une commande peut contenir plusieurs boissons, et elle a un total. Un client peut annuler une commande, tant qu’elle n’a pas été préparée. »
Étape A — le marquage :
- Noms : client, boisson, menu, commande, nom, taille, prix, total.
- Verbes : commander, contenir, annuler, préparer.
- Règles : grande = +3. Annulation seulement avant la préparation.
Étape B — les quatre questions :
| Candidat | Identité/données ? | Comportement ? | Plusieurs instances ? | Décision |
|---|---|---|---|---|
| Boisson | ✓ nom, taille, prix | ✓ calcul du prix selon la taille | ✓ | Classe Drink |
| Commande | ✓ articles, statut | ✓ total, annulation, préparation | ✓ | Classe Order |
| Client | ✓ nom | Un peu | ✓ | Ici : un attribut (le nom) de Order. L’exigence ne lui donne aucun comportement. |
| Menu | Une liste de boissons | Peut-être une recherche | Un seul | Ici : un simple dict. Si des règles s’ajoutent, il deviendra une classe. |
| Taille | Une valeur (« small »/« large ») | — | — | Un attribut de Drink |
| Prix | Un nombre | — | — | Un attribut + une méthode calculée |
| Total | Dérivé | — | — | Une méthode Order.total() |
Étape C — les méthodes (« qui détient les données ? ») :
price()→ Drink, car Drink connaît le prix de base et la taille.total()→ Order, car Order détient les boissons.cancel(),mark_prepared()→ Order, car Order détient le statut.
Étape D — le code. week-3/coffee_shop.py :
LARGE_EXTRA = 3
class Drink:
def __init__(self, name, base_price, size="small"):
if size not in ("small", "large"):
raise ValueError("size must be small or large, got " + size)
self.name = name
self.base_price = base_price
self.size = size
def price(self):
if self.size == "large":
return self.base_price + LARGE_EXTRA
return self.base_price
class Order:
def __init__(self, customer_name):
self.customer_name = customer_name
self.drinks = []
self.status = "open"
def add(self, drink):
if self.status != "open":
raise ValueError("cannot add drinks to a " + self.status + " order")
self.drinks.append(drink)
def total(self):
return sum(drink.price() for drink in self.drinks)
def mark_prepared(self):
self.status = "prepared"
def cancel(self):
if self.status == "prepared":
raise ValueError("cannot cancel - the order was already prepared")
self.status = "cancelled"
MENU = {"espresso": 9, "latte": 14, "tea": 8}
order = Order("Dana")
order.add(Drink("latte", MENU["latte"], "large"))
order.add(Drink("espresso", MENU["espresso"]))
print(order.customer_name, "pays", order.total())
order.mark_prepared()
try:
order.cancel()
except ValueError as error:
print("Rejected:", error)Sortie :
Dana pays 26
Rejected: cannot cancel - the order was already preparedAnalyse du code ligne par ligne
| Décision de conception | À quoi elle ressemble dans le code |
|---|---|
| Boisson = une classe | class Drink avec des données (nom, prix, taille) et un comportement (price). |
| Taille = un attribut avec une règle | self.size, avec une validation dans __init__. |
| « Grande = +3 » appartient à la boisson | Drink.price(), car la boisson connaît sa propre taille. |
| Commande = une classe | class Order avec des articles et un statut. |
| Client = un attribut (pour l’instant) | self.customer_name, une chaîne. |
| « Annulation seulement avant la préparation » | Une validation dans Order.cancel(). |
| Menu = un dict | MENU. Il n’a pas de comportement, il n’y a donc pas besoin d’une classe. |
| Total = une méthode | total() calcule à partir de drinks, et n’est pas stocké comme une donnée qui risque de devenir obsolète. |
Un autre exemple
Quand l’exigence change — le client devient une classe. Une exigence a été ajoutée : « Les clients ont une carte de fidélité : chaque achat rapporte un point par tranche de 10 NIS, et à 50 points on obtient une boisson gratuite. »
Maintenant, le client a des données (des points), un comportement (gagner, échanger) et des règles (50 points). Selon les quatre questions, c’est déjà une classe :
POINTS_PER_SHEKEL = 0.1
FREE_DRINK_POINTS = 50
class Customer:
def __init__(self, name):
self.name = name
self.points = 0
def earn_points(self, amount_paid):
self.points = self.points + int(amount_paid * POINTS_PER_SHEKEL)
def has_free_drink(self):
return self.points >= FREE_DRINK_POINTS
def redeem_free_drink(self):
if not self.has_free_drink():
raise ValueError(self.name + " has only " + str(self.points) + " points")
self.points = self.points - FREE_DRINK_POINTS
dana = Customer("Dana")
for payment in [120, 260, 140]:
dana.earn_points(payment)
print(dana.name, "has", dana.points, "points. Free drink?", dana.has_free_drink())
dana.redeem_free_drink()
print("After redeeming:", dana.points)Sortie :
Dana has 52 points. Free drink? True
After redeeming: 2Et dans la structure : Order contient maintenant self.customer (un objet Customer) au lieu de customer_name, et quand une commande est payée, elle appelle self.customer.earn_points(self.total()). Le même concept, « client », a commencé comme un attribut et est devenu une classe quand les exigences ont grandi. C’est naturel, et c’est le bon processus de travail.
Résumé
- Les noms → candidats pour des classes/attributs. Les verbes → des méthodes. Les règles → la validation.
- Une classe quand il y a identité + données, comportement ou règles, et plusieurs instances. Une valeur unique sans comportement → un attribut.
- Le même concept peut être un attribut dans un système et une classe dans un autre. Commencez simple, et faites évoluer quand les exigences grandissent.
- Une méthode appartient à la classe qui détient les données dont elle a besoin (Information Expert).
- Parfois, une « action » (un prêt, une commande) est en réalité une classe, quand elle a ses propres données et sa propre logique.
- Pas une classe pour chaque nom, et pas une God Class qui fait tout.
Documentation officielle
Chapitre 7 : Retour à la banque — tout ensemble
Qu’allons-nous apprendre ?
Nous revenons au système bancaire du début de la leçon, et nous le reconstruisons — cette fois avec tout ce que nous avons appris dans les chapitres précédents.
Pourquoi en avons-nous besoin ?
Chaque chapitre a ajouté un outil et résolu un problème. Ici, vous voyez comment ils fonctionnent tous ensemble dans une seule classe — et c’est exactement ce que vous ferez seuls dans le mini-projet.
La banque — avec tout ce que nous avons appris
class BankAccount:
accounts_opened = 0 # shared by all accounts
def __init__(self, owner, balance=0):
if balance < 0:
raise ValueError("balance cannot be negative")
self.owner = owner # per account
self.balance = balance
BankAccount.accounts_opened = BankAccount.accounts_opened + 1
def deposit(self, amount): # setter - changes the state
if amount <= 0:
raise ValueError("amount must be positive")
self.balance = self.balance + amount
def __str__(self):
return self.owner + ": " + str(self.balance)
dana = BankAccount("Dana", 1000)
dana.deposit(200)
print(dana) # Dana: 1200| Partie du code | De quel chapitre |
|---|---|
class BankAccount — un seul modèle pour tous les comptes | Chapitre 2 |
__init__ avec une vérification — chaque compte naît complet et valide | Chapitre 3 |
deposit — un Setter qui protège les règles | Chapitre 4 |
__str__ — à quoi ressemble le compte à l’affichage | Chapitre 4 |
accounts_opened — un compteur partagé par tous les comptes | Chapitre 5 |
Les quatre problèmes du premier chapitre ont été résolus : on ne peut pas créer un compte avec un solde négatif, les données et les actions sont ensemble, la structure a une seule définition, et la classe elle-même impose les règles.
Résumé
- Une classe est un modèle, et un objet est une instance construite à partir de lui — avec ses propres données.
__init__s’exécute à la création et garantit que l’objet naît complet ;selfest l’objet courant.- Les méthodes sont les actions : un Getter renvoie une information, un Setter modifie l’état et vérifie les règles, et
__str__décide de ce qu’afficheprint. - Un Class Attribute est une donnée unique partagée par tous les objets — comme un compteur.
Documentation officielle
Exercices
Les exercices n’utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d’abord seul, et n’ouvrez la solution qu’ensuite.
Ce que vous pratiquez sur cette page : pourquoi la POO est nécessaire, créer une classe avec __init__ et la vérification des valeurs, les méthodes (Getter et Setter) et __str__, les données partagées par toute la classe, et la conception — ce qui est une classe, un attribut et une méthode.
À la fin de la page, vous saurez :
- Reconnaître quand il vaut mieux réunir des données et des actions dans un seul objet
- Écrire une classe complète :
__init__, des méthodes et__str__ - Utiliser un Class Attribute comme compteur partagé par tous les objets
- Transformer une description verbale en liste de classes, d’attributs et de méthodes
Exercice 1: Repérer les problèmes
Ce que vous faites : lire un code existant et y trouver des problèmes — sans écrire de code.
Le code :
student1 = {"name": "Dana", "grades": [90, 85]}
student2 = {"name": "Avi", "grades": [70]}
student3 = {"nmae": "Noa", "grades": []}
def average(student):
return sum(student["grades"]) / len(student["grades"])La tâche : trouvez trois problèmes qui viennent du fait que les données (les dictionnaires) et les actions (la fonction) sont séparées. Pour chaque problème, écrivez une phrase : quel est le problème, et à quelle ligne il apparaît.
Ce qu’il faut rendre : trois problèmes, une phrase pour chacun.
Afficher la solution
- Une faute de frappe crée une nouvelle clé —
student3a"nmae"au lieu de"name". Aucune structure définie ne le détecterait, et le problème ne sera découvert que lorsque quelqu’un essaierastudent3["name"]. - Il n’y a pas de règles — n’importe quel code peut mettre
-50ou"abc"dansgrades, et personne ne vérifie. - Les données et les actions sont dispersées —
averageest une fonction séparée qui reçoit un dictionnaire, et ne sait même pas si c’est un étudiant. Et avec une liste vide (student3), elle échouera avec uneZeroDivisionError.
En POO, Student définit une fois pour toutes quelles données il possède et quelles actions il sait faire — tout ensemble.
Exercice 2: Une vérification à la création
Ce que vous construisez : un produit dans une boutique — qu’on ne peut pas créer avec un prix négatif.
Ce qu’il faut écrire :
- Une classe nommée
Product. - Un
__init__qui reçoit un nom, un prix et une quantité. Si la quantité n’a pas été passée — elle vaut 0. - Si le prix est négatif : lancez une
ValueErroravec le message qui apparaît dans la sortie, et n’enregistrez rien. - Sinon : enregistrez les trois données sur l’objet, avec
self.
Code de test :
pen = Product("Pen", 5)
print(pen.name, pen.price, pen.quantity)
try:
Product("Bad", -10)
except ValueError as error:
print("error:", error)La sortie souhaitée :
Pen 5 0
error: price cannot be negativeAfficher la solution
class Product:
def __init__(self, name, price, quantity=0):
if price < 0:
raise ValueError("price cannot be negative")
self.name = name
self.price = price
self.quantity = quantity
pen = Product("Pen", 5)
print(pen.name, pen.price, pen.quantity)
try:
Product("Bad", -10)
except ValueError as error:
print("error:", error)Pen 5 0
error: price cannot be negativeLa vérification dans __init__ garantit qu’il n’existe jamais d’objet Product avec un prix invalide.
Exercice 3: Student avec des méthodes et str
Ce que vous construisez : un étudiant qui gère ses propres notes.
Ce qu’il faut écrire :
- Une classe nommée
Student. Le__init__reçoit un nom et crée une liste de notes vide. add_grade— reçoit une note et l’ajoute à la liste. C’est un Setter : il modifie l’état.average— renvoie la moyenne, ou 0 s’il n’y a pas encore de notes. C’est un Getter : il ne fait que renvoyer.__str__— renvoie le nom et la moyenne, exactement comme dans la dernière ligne de la sortie.
Code de test :
dana = Student("Dana")
print(dana.average())
dana.add_grade(90)
dana.add_grade(75)
print(dana.average())
print(dana)La sortie souhaitée :
0
82.5
Dana (average 82.5)Afficher la solution
class Student:
def __init__(self, name):
self.name = name
self.grades = []
def add_grade(self, grade):
self.grades.append(grade)
def average(self):
if len(self.grades) == 0:
return 0
return sum(self.grades) / len(self.grades)
def __str__(self):
return self.name + " (average " + str(self.average()) + ")"
dana = Student("Dana")
print(dana.average())
dana.add_grade(90)
dana.add_grade(75)
print(dana.average())
print(dana)0
82.5
Dana (average 82.5)add_grade est un Setter — il modifie l’état. average est un Getter — il renvoie une information sans rien modifier. print appelle __str__ automatiquement.
Exercice 4: Un compteur partagé
Ce que vous construisez : un produit qui connaît son prix TVA comprise — et une classe qui compte combien de produits ont été créés.
Ce qu’il faut écrire :
- Une classe nommée
Product, avec un__init__qui reçoit un nom et un prix. - Deux Class Attributes — des données qui appartiennent à toute la classe, pas à un seul objet :
VAT_RATEavec la valeur 0.17, etcreated_countqui commence à 0. - Chaque fois qu’un produit est créé, augmentez
created_countde 1. price_with_vat— renvoie le prix TVA comprise.
Code de test :
laptop = Product("Laptop", 1000)
mouse = Product("Mouse", 50)
print(Product.created_count)
print(laptop.price_with_vat())La sortie souhaitée :
2
1170.0Afficher la solution
class Product:
VAT_RATE = 0.17
created_count = 0
def __init__(self, name, price):
self.name = name
self.price = price
Product.created_count = Product.created_count + 1
def price_with_vat(self):
return round(self.price * (1 + Product.VAT_RATE), 2)
laptop = Product("Laptop", 1000)
mouse = Product("Mouse", 50)
print(Product.created_count)
print(laptop.price_with_vat())2
1170.0nameetpriceappartiennent à chaque produit séparément — des Instance Attributes.VAT_RATEetcreated_countsont partagés par tous les produits — des Class Attributes.- Le compteur est mis à jour via
Product.created_count— et non viaself.
Exercice 5: Noms et verbes
Ce que vous faites : de la conception uniquement, sans code — transformer une description verbale en liste de classes, d’attributs et de méthodes.
L’exigence :
« Dans un café, des clients commandent des boissons de différentes tailles. Chaque boisson a un prix de base, et une grande boisson coûte 3 NIS de plus. Le client gagne des points à chaque commande, et peut les échanger. »
La tâche : construisez un tableau à trois colonnes : le candidat, ce que c’est (une classe, un attribut ou une méthode), et pourquoi.
Indice : les noms sont des candidats pour des classes ou des attributs. Les verbes sont des candidats pour des méthodes.
Ce qu’il faut rendre : un tableau avec tous les candidats de l’exigence.
Afficher la solution
| Candidat | Ce que c’est | Pourquoi |
|---|---|---|
Client (Customer) | Classe | Un nom avec des données (nom, points) et un comportement (gagne, échange) |
Boisson (Drink) | Classe | Un nom avec des données (prix de base, taille) et une règle (la grande coûte plus cher) |
| Prix de base, taille | Attributs de Drink | Des valeurs simples qui décrivent une boisson |
| Points | Attribut de Customer | Un simple nombre |
| Gagne des points, échange | Méthodes de Customer | Des verbes — des actions sur les données du client |
| Prix final | Méthode de Drink | Calculé à partir du prix et de la taille |
Mini-projet: Une petite bibliothèque
Quel système vous construisez : un petit système pour une bibliothèque. La bibliothèque a des livres, et chaque livre a plusieurs exemplaires. Les lecteurs empruntent des exemplaires et les rendent, et à chaque instant le système sait combien d’exemplaires de chaque livre sont libres — et combien de livres il y a au total dans la bibliothèque.
Ce que vous apprendrez du projet :
- Construire une classe complète à partir de zéro :
__init__, des méthodes et__str__ - Vérifier les valeurs dès la création, et lancer une erreur quand elles sont invalides
- Distinguer un Setter qui modifie l’état d’un Getter qui ne fait que renvoyer une information
- Utiliser un Class Attribute comme compteur partagé par tous les objets
Ce qu’il faut construire — étape par étape :
Étape 1 — la classe et le compteur partagé. Écrivez une classe nommée Book, avec un Class Attribute nommé total_books qui commence à 0. Il comptera combien de livres ont été créés.
Étape 2 — créer un livre. Le __init__ reçoit un titre, un auteur et un nombre d’exemplaires, et les enregistre. Au départ, tous les exemplaires sont libres — enregistrez-les aussi dans available. Chaque nouveau livre augmente total_books de 1. Si le nombre d’exemplaires est inférieur à 1 — lancez une ValueError, et ne créez pas de livre.
Étape 3 — emprunter et rendre (Setters).
borrow— retire un exemplaire libre. S’il n’y a pas d’exemplaire libre :ValueError.return_book— rend un exemplaire. Si tous les exemplaires sont déjà à la bibliothèque :ValueError.
Étape 4 — vérifier (Getter). is_available renvoie True s’il y a au moins un exemplaire libre, sinon False.
Étape 5 — l’affichage. __str__ renvoie le titre, l’auteur et combien d’exemplaires sont libres sur combien, comme dans la sortie.
Les messages d’erreur — exactement ainsi :
copies must be at least 1
no copies available
all copies are already inCode de test :
books = [
Book("Clean Code", "Martin", 3),
Book("Python Basics", "Cohen", 1),
]
books[0].borrow()
books[1].borrow()
try:
books[1].borrow()
except ValueError as error:
print("Python Basics ->", error)
try:
Book("Empty", "Nobody", 0)
except ValueError as error:
print("Empty ->", error)
for book in books:
print(book, "| available:", book.is_available())
print("Books in the library:", Book.total_books)La sortie souhaitée :
Python Basics -> no copies available
Empty -> copies must be at least 1
Clean Code by Martin (2/3 available) | available: True
Python Basics by Cohen (0/1 available) | available: False
Books in the library: 2À la fin du projet, vous saurez :
- Traduire la description d’une « chose » du monde — un livre — en une classe avec des données et des actions
- Garder les règles à l’intérieur de l’objet, pour qu’il ne puisse pas atteindre un état invalide
- Comprendre pourquoi le livre Empty n’est pas compté par le compteur : l’erreur a été lancée avant l’incrémentation
Bonus : ajoutez une classe nommée Library qui contient une liste de livres, avec add_book qui ajoute un livre, et available_books qui ne renvoie que les livres qu’on peut emprunter en ce moment.
Afficher la solution
class Book:
total_books = 0
def __init__(self, title, author, copies):
if copies < 1:
raise ValueError("copies must be at least 1")
self.title = title
self.author = author
self.copies = copies
self.available = copies
Book.total_books = Book.total_books + 1
def borrow(self):
if self.available == 0:
raise ValueError("no copies available")
self.available = self.available - 1
def return_book(self):
if self.available == self.copies:
raise ValueError("all copies are already in")
self.available = self.available + 1
def is_available(self):
return self.available > 0
def __str__(self):
return (self.title + " by " + self.author + " ("
+ str(self.available) + "/" + str(self.copies) + " available)")
books = [
Book("Clean Code", "Martin", 3),
Book("Python Basics", "Cohen", 1),
]
books[0].borrow()
books[1].borrow()
try:
books[1].borrow()
except ValueError as error:
print("Python Basics ->", error)
try:
Book("Empty", "Nobody", 0)
except ValueError as error:
print("Empty ->", error)
for book in books:
print(book, "| available:", book.is_available())
print("Books in the library:", Book.total_books)Python Basics -> no copies available
Empty -> copies must be at least 1
Clean Code by Martin (2/3 available) | available: True
Python Basics by Cohen (0/1 available) | available: False
Books in the library: 2total_booksest un Class Attribute — un seul compteur pour tous les livres. Le livreEmptyn’a pas été créé (l’erreur a été lancée avant l’incrémentation), le compteur vaut donc 2.borrowetreturn_booksont des Setters — ils modifient l’état, et vérifient les règles avant la modification.is_availableest un Getter — il renvoie une information sans rien modifier.
Bonus :
class Library:
def __init__(self):
self.books = []
def add_book(self, book):
self.books.append(book)
def available_books(self):
result = []
for book in self.books:
if book.is_available():
result.append(book)
return result
library = Library()
for book in books:
library.add_book(book)
for book in library.available_books():
print(book)Clean Code by Martin (2/3 available)