
Ton script tourne. Il est fait de fonctions, chacune fait son travail, et pourtant chaque nouvelle fonctionnalité oblige à traîner une variable de plus d’un appel à l’autre. Un jour, tu ouvres le code d’un collègue et tu y trouves self, __init__, des classes qui héritent d’autres classes.
Bonne nouvelle : ce saut n’est pas un changement de langage, c’est un changement de rangement. Voici comment le franchir sans te perdre.
L’essentiel en 30 secondes
- Une classe regroupe au même endroit des données (ses attributs) et les fonctions qui travaillent dessus (ses méthodes).
- Le constructeur
__init__()prépare chaque objet ; les variables d’instance sont propres à chaque objet, les variables de classe sont partagées. - Les méthodes magiques (
__str__(),__lt__(),__iter__()…) apprennent à tes objets à réagir aux opérateurs et aux boucles de Python. - L’héritage permet de réutiliser une classe existante ; le polymorphisme permet à une même fonction de traiter des objets de classes différentes.
- En Python, rien n’est vraiment privé : c’est une affaire de convention.
L’intuition : ranger ce qui va ensemble
Imagine un petit jeu à deux joueurs. Il te faut deux scores, une opération pour ajouter des points, une pour remettre les compteurs à zéro, une pour afficher le score et une pour désigner le vainqueur. Avec de simples fonctions, il faut passer les scores de l’une à l’autre à chaque appel, et c’est à toi de ne jamais les mélanger.
L’approche orientée objet propose autre chose : fabriquer un seul objet qui possède les deux scores et sait faire ces quatre opérations. Tu ne manipules plus des variables éparpillées, mais un « suivi de score » auquel tu donnes des ordres. C’est exactement ce que fait la classe ScoreTracker un peu plus bas.
💡 Intuition Une classe, c’est un plan de fabrication ; chaque objet créé à partir de ce plan emporte ses propres données avec lui, et connaît les opérations qui s’y appliquent.
Sous le capot : les briques d’une classe
Une classe, un constructeur, un attribut
Voici la classe la plus simple qui soit : elle a un constructeur __init__() qui pose un attribut x, et une méthode qui l’affiche.
#!/usr/bin/env python
class MyClass:
def __init__(self):
self.x = 5
def main(self):
print('inside main')
print('x:',self.x)
if __name__ == '__main__':
myClass = MyClass()
myClass.main()
Le bloc if __name__ == '__main__': est un idiome que tu croiseras partout : il n’exécute son contenu que si le fichier est lancé directement, jamais lorsqu’il est simplement importé par un autre programme. Trois façons d’écrire l’en-tête existent (class MyClass:, class MyClass(): ou class MyClass(object):), mais elles sont équivalentes : en Python 3, toute classe hérite implicitement de object.
Ce qui appartient à l’objet, ce qui appartient à la classe
Une distinction change tout : une variable d’instance est propre à chaque objet, une variable de classe est partagée par toutes les instances.
class Company:
# a shared class variable:
location = "USA"
def __init__(self, name):
# an instance variable:
self.name = name
Ici, location est identique pour toutes les entreprises créées ; name, lui, est défini à la création et diffère d’un objet à l’autre.
Public, protégé, privé : une question de politesse
En Python 3, tout est public par défaut. Un tiret bas en préfixe (_variable) signale une variable protégée, réservée à la classe et à ses sous-classes ; un double tiret bas (__variable) signale une variable privée, réservée à la classe elle-même. Mais rien n’empêche techniquement d’y accéder de l’extérieur, contrairement à Java où le compilateur l’interdit.
⚠️ Piège Un attribut « privé » en Python n’est privé que par convention : c’est ton engagement d’équipe qui le protège, pas le langage.
Le voir tourner : ton premier objet utile
Reprenons notre jeu à deux joueurs. Observe comment les deux scores et les quatre opérations vivent dans la même classe, et comment le code appelant se contente de donner des ordres à un objet.
class ScoreTracker:
def __init__(self, player1_initial=0,
player2_initial=0):
self.player1 = player1_initial
self.player2 = player2_initial
def update_score(self, player_number,
new_points=None):
if player_number == 1:
self.player1 += (new_points or 1)
elif player_number == 2:
self.player2 += (new_points or 1)
else:
raise ValueError("Player number must be 1 or 2")
def reset_scores(self):
self.player1 = 0
self.player2 = 0
def display_scores(self, format_style="{} - {}"):
return format_style.format(self.player1, self.player2)
def get_winner(self):
if self.player1 > self.player2:
return "Player 1"
elif self.player2 > self.player1:
return "Player 2"
else:
return "Tie"
# Example usage
score_game = ScoreTracker()
score_game.update_score(1)
score_game.update_score(2, new_points=5) # Player 2 scores additional points
print(score_game.display_scores())
print("Current score:", score_game.player1, "-",
score_game.player2)
if score_game.get_winner():
print(f"Winner is {score_game.get_winner()}!")
score_game.reset_scores()
print("Reset scores:", score_game.display_scores())
Trois choses à retenir de cet exemple. Les scores sont créés une fois, dans le constructeur, avec 0 par défaut. Chaque méthode travaille sur ces mêmes scores sans qu’on ait à les lui repasser. Enfin, l’appel update_score(2, new_points=5) montre qu’une méthode se règle avec des arguments, exactement comme une fonction : ta culture des fonctions reste donc entièrement valable.
Cacher le « comment » derrière un « quoi »
Une classe peut aussi masquer sa cuisine interne. Ici, le code appelant ne connaît que search_list() ; le choix entre une recherche linéaire et une recherche binaire se fait à l’intérieur, dans des méthodes préfixées par __.
class MyEncapsulation:
def __init__(self,my_list):
self.my_list = my_list
self.sorted = False
# invokes a private method:
def search_list(self,item):
self.__search_my_list(item)
def __search_my_list(self,item):
if(self.sorted == False):
# perform a linear search:
self.__linear_search(item)
else:
# perform a binary search:
self.__binary_search(item)
def __linear_search(self,item):
# perform a linear search
pass
def __binary_search(self,item):
# perform a binary search
pass
if __name__ == '__main__':
enc1 = MyEncapsulation([4,1,5,3,8])
enc1.search_list(5)
Le bénéfice est concret : demain, on peut remplacer la recherche linéaire par une binaire, ou l’inverse, sans changer la signature publique search_list() ni le code qui l’appelle. On appelle cela l’encapsulation : faire évoluer l’intérieur d’une classe sans casser ce qui en dépend.
Faire parler tes objets avec les méthodes magiques
Les méthodes dont le nom commence et finit par deux tirets bas (__add__(), __len__(), __str__()…) s’appellent des méthodes magiques, ou dunders. Elles permettent à une classe personnalisée de répondre aux opérateurs standards de Python.
Deux d’entre elles décident de l’affichage : __str__() vise un texte lisible pour l’utilisateur final, __repr__() un texte utile au développeur. Si l’une manque, Python retombe sur l’autre, et print() appelle __str__() par défaut.
Autre exemple : pour dire ce que signifie « inférieur à » entre deux points en 3D, on écrit __lt__(). On peut ensuite comparer deux objets avec l’opérateur <, comme n’importe quel type natif.
def __lt__(self, other):
if self.x < other.x and self.y < other.y and self.z < other.z:
return True
else:
return False
if(p1 < p2): print("p1 < p2")
elif (p2 < p1): print("p2 < p1")
else: print("cannot be compared")
Ce même __lt__() est ce qu’utilise queue.PriorityQueue pour savoir quelle tâche traiter en premier : elle compare les tâches entre elles, et les traite par priorité croissante, quel que soit leur ordre d’insertion. Tes classes deviennent ainsi compatibles avec les outils de la bibliothèque standard.
💡 Intuition Une méthode magique, c’est un contrat : « si tu me fournis cette méthode, Python saura s’en servir dans ses opérateurs et ses boucles ».
Une classe qui se laisse parcourir
Le même principe s’applique aux boucles for. Un objet est itérable dès qu’il définit __iter__(), qui doit renvoyer un itérateur, c’est-à-dire un objet doté de __next__(), chargé de produire l’élément suivant ou de lever StopIteration quand tout est épuisé.

iter(obj) déclenche __iter__(), qui renvoie un itérateur, puis la boucle for réclame chaque élément avec __next__() jusqu’à l’exception finale.Réutiliser sans copier : héritage et polymorphisme
L’héritage : « fais comme ma classe parente »
Quand une classe B hérite d’une classe A, tout ce que A définit devient accessible depuis B. Dans l’exemple ci-dessous, la méthode func() de MyClassB est mise en commentaire : c’est donc celle de MyClassA qui s’exécute quand on appelle instanceB.func().
class MyClassA:
def __init__(self):
print('Hello from A')
def func(self):
print('Hello again from A')
class MyClassB(MyClassA):
def __init__(self):
print('Hello from B')
#def func(self):
# print('Hello again from B')
if __name__ == '__main__':
instanceA = MyClassA()
instanceB = MyClassB()
print()
print('instanceA:')
instanceA.func()
print('instanceB:')
instanceB.func()

MyClassB ne redéfinit pas func(), Python remonte vers la classe parente pour la trouver.Un contrat pour les sous-classes
Une classe de base peut aussi servir de simple contrat. Dans un exemple de préférences alimentaires, la classe parente MyFoodPrefs lève volontairement NotImplementedError dans chacune de ses méthodes : elle impose aux sous-classes MyVegetarian et MyCarnivore d’écrire leur propre version. Chacune appelle super().__init__() pour déclencher le constructeur de la classe parente. La version végétarienne refuse d’ajouter « Steak » ou « Chicken », la version carnivore n’applique jamais ce filtre.
Le polymorphisme : une fonction, plusieurs classes
C’est ici que tes fonctions retrouvent leur place. Le polymorphisme désigne la capacité d’une même fonction à opérer sur des objets de classes différentes, pourvu qu’ils exposent l’interface attendue. Regarde calcPerimeter() : elle ne teste jamais le type de son argument.
class MyRect:
def perimeter(self):
print('Perimeter of a rectangle')
class MySquare:
def perimeter(self):
print('Perimeter of a square')
def calcPerimeter(obj):
obj.perimeter()
if __name__ == '__main__':
instanceR = MyRect()
instanceS = MySquare()
print('instanceR:')
calcPerimeter(instanceR)
print('instanceS:')
calcPerimeter(instanceS)
MyRect et MySquare n’ont aucun lien de parenté : seul compte le fait que les deux exposent une méthode perimeter(). C’est le duck typing : la fonction ne se soucie pas de la classe exacte, seulement de savoir si l’objet « se comporte comme un canard ». Java exigerait une interface commune ; en Python, cette exigence reste implicite. Pour formaliser malgré tout le contrat, le module abc (Abstract Base Class) permet de définir des classes abstraites qui imposent les méthodes à implémenter.
📌 À retenir Passer aux objets ne t’oblige pas à jeter tes fonctions : une fonction qui accepte « n’importe quel objet sachant faire ceci » est l’une des formes les plus élégantes du polymorphisme.
Pièges et limites
-
Constructeurs jamais appelés. Dans l’exemple d’héritage multiple,
MyClassC(MyClassA, MyClassB)définit son propre__init__()sans appeler ceux de ses parents : ils ne s’exécutent donc pas. Pense àsuper().__init__()quand tu veux le comportement du parent. -
Héritage multiple : l’ordre compte. Quand deux parents définissent la même méthode, Python la cherche de gauche à droite dans la liste des parents. Avec
MyClassC(MyClassA, MyClassB), c’est celle deMyClassAqui gagne ; inverse l’ordre et le résultat bascule. Le moduleinspectte montre cet ordre de résolution (le MRO) avecinspect.getmro(MaClasse). -
Toujours passer par
iter(). Appeler directement__iter__()ne déclenche aucune vérification, alors que la fonctioniter()lève uneTypeErrorsi__iter__()ne renvoie pas un vrai itérateur. -
L’addition n’est pas symétrique.
myobj1 + 7fonctionne grâce à__add__(), mais7 + myobj1échoue tant que tu n’as pas écrit__radd__(). Même principe pour__sub__()/__rsub__()et__mul__()/__rmul__(). -
Ne compte pas sur
__del__(). Son exécution n’est pas garantie : pour libérer un fichier ou une connexion, ferme-les explicitement.

A2.Aller plus loin
__init__et sa fratrie : trois méthodes encadrent la vie d’un objet —__new__()le crée,__init__()l’initialise,__del__()intervient à sa destruction.- Le protocole d’itération : c’est le mécanisme qui se cache derrière toute boucle
for, et que ta classe peut implémenter à son tour. - Les collections d’objets : une classe qui encapsule une liste d’objets « valeur » (des employés, par exemple) est un motif que l’on retrouve dans la plupart des applications réelles.
- Tes propres structures de données : liste chaînée, file de priorité… rien n’empêche de les construire avec des classes personnalisées.
À retenir
- Une classe range au même endroit un état et les opérations qui le modifient : tu donnes des ordres à un objet au lieu de faire circuler des variables.
- Ce que tu sais des fonctions (arguments, valeurs par défaut, retours) s’applique tel quel aux méthodes.
- Les méthodes magiques rendent tes objets compatibles avec les opérateurs, les comparaisons et les boucles de Python.
- L’héritage réutilise une classe ; le duck typing permet de ne pas en avoir besoin quand seule l’interface compte.
- Python te fait confiance : privé et protégé sont des conventions, pas des barrières.